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

Add overflow menu to mini context menu #34031

Merged
merged 17 commits into from
Jan 18, 2024
13 changes: 13 additions & 0 deletions assets/images/chatbubble-add.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions assets/images/chatbubble-unread.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3106,6 +3106,8 @@ const CONST = {
EMAIL: 'EMAIL',
REPORT: 'REPORT',
},

MINI_CONTEXT_MENU_MAX_ITEMS: 4,
} as const;

export default CONST;
7 changes: 4 additions & 3 deletions src/components/ContextMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef, useImperativeHandle} from 'react';
import type {GestureResponderEvent} from 'react-native';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useThrottledButtonState from '@hooks/useThrottledButtonState';
Expand Down Expand Up @@ -27,7 +28,7 @@ type ContextMenuItemProps = {
isMini?: boolean;

/** Callback to fire when the item is pressed */
onPress: () => void;
onPress: (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => void;

/** A description text to show under the title */
description?: string;
Expand All @@ -52,11 +53,11 @@ function ContextMenuItem(
const {windowWidth} = useWindowDimensions();
const [isThrottledButtonActive, setThrottledButtonInactive] = useThrottledButtonState();

const triggerPressAndUpdateSuccess = () => {
const triggerPressAndUpdateSuccess = (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => {
if (!isThrottledButtonActive) {
return;
}
onPress();
onPress(event);

// We only set the success state when we have icon or text to represent the success state
// We may want to replace this check by checking the Result from OnPress Callback in future.
Expand Down
4 changes: 4 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import Camera from '@assets/images/camera.svg';
import Car from '@assets/images/car.svg';
import Cash from '@assets/images/cash.svg';
import Chair from '@assets/images/chair.svg';
import ChatBubbleAdd from '@assets/images/chatbubble-add.svg';
import ChatBubbleUnread from '@assets/images/chatbubble-unread.svg';
import ChatBubble from '@assets/images/chatbubble.svg';
import ChatBubbles from '@assets/images/chatbubbles.svg';
import Checkmark from '@assets/images/checkmark.svg';
Expand Down Expand Up @@ -264,4 +266,6 @@ export {
Podcast,
Linkedin,
Instagram,
ChatBubbleAdd,
ChatBubbleUnread,
};
6 changes: 4 additions & 2 deletions src/components/Reactions/MiniQuickEmojiReactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import * as EmojiUtils from '@libs/EmojiUtils';
import getButtonState from '@libs/getButtonState';
import variables from '@styles/variables';
import * as EmojiPickerAction from '@userActions/EmojiPickerAction';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -71,7 +72,7 @@ function MiniQuickEmojiReactions({

return (
<View style={styles.flexRow}>
{CONST.QUICK_REACTIONS.map((emoji: Emoji) => (
{CONST.QUICK_REACTIONS.slice(0, 3).map((emoji: Emoji) => (
<BaseMiniContextMenuItem
key={emoji.name}
isDelayButtonStateComplete={false}
Expand Down Expand Up @@ -100,7 +101,8 @@ function MiniQuickEmojiReactions({
>
{({hovered, pressed}) => (
<Icon
small
width={variables.iconSizeMedium}
height={variables.iconSizeMedium}
src={Expensicons.AddReaction}
fill={StyleUtils.getIconFillColor(getButtonState(hovered, pressed, false))}
/>
Expand Down
5 changes: 3 additions & 2 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,9 +454,10 @@ export default {
deleteConfirmation: ({action}: DeleteConfirmationParams) => `Are you sure you want to delete this ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'request' : 'comment'}?`,
onlyVisible: 'Only visible to',
replyInThread: 'Reply in thread',
subscribeToThread: 'Subscribe to thread',
unsubscribeFromThread: 'Unsubscribe from thread',
joinThread: 'Join thread',
leaveThread: 'Leave thread',
flagAsOffensive: 'Flag as offensive',
menu: 'Menu',
},
emojiReactions: {
addReactionTooltip: 'Add reaction',
Expand Down
5 changes: 3 additions & 2 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,10 @@ export default {
`¿Estás seguro de que quieres eliminar este ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'pedido' : 'comentario'}`,
onlyVisible: 'Visible sólo para',
replyInThread: 'Responder en el hilo',
subscribeToThread: 'Suscribirse al hilo',
unsubscribeFromThread: 'Darse de baja del hilo',
joinThread: 'Unirse al hilo',
leaveThread: 'Dejar hilo',
flagAsOffensive: 'Marcar como ofensivo',
menu: 'Menú',
},
emojiReactions: {
addReactionTooltip: 'Añadir una reacción',
Expand Down
12 changes: 9 additions & 3 deletions src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,15 @@ function BaseReportActionContextMenu(props) {
props.isPinnedChat,
props.isUnreadChat,
isOffline,
props.isMini,
);

const shouldEnableArrowNavigation = !props.isMini && (props.isVisible || shouldKeepOpen);
const filteredContextMenuActions = _.filter(ContextMenuActions, shouldShowFilter);

let filteredContextMenuActions = _.filter(ContextMenuActions, shouldShowFilter);
filteredContextMenuActions =
props.isMini && filteredContextMenuActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS
? [...filteredContextMenuActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1), filteredContextMenuActions.at(-1)]
: filteredContextMenuActions;
// Context menu actions that are not rendered as menu items are excluded from arrow navigation
const nonMenuItemActionIndexes = _.map(filteredContextMenuActions, (contextAction, index) => (_.isFunction(contextAction.renderContent) ? index : undefined));
const disabledIndexes = _.filter(nonMenuItemActionIndexes, (index) => !_.isUndefined(index));
Expand Down Expand Up @@ -143,6 +147,8 @@ function BaseReportActionContextMenu(props) {
close: () => setShouldKeepOpen(false),
openContextMenu: () => setShouldKeepOpen(true),
interceptAnonymousUser,
anchor: props.anchor,
checkIfContextMenuActive: props.checkIfContextMenuActive,
};

if (contextAction.renderContent) {
Expand All @@ -165,7 +171,7 @@ function BaseReportActionContextMenu(props) {
successText={contextAction.successTextTranslateKey ? props.translate(contextAction.successTextTranslateKey) : undefined}
isMini={props.isMini}
key={contextAction.textTranslateKey}
onPress={() => interceptAnonymousUser(() => contextAction.onPress(closePopup, payload), contextAction.isAnonymousAction)}
onPress={(event) => interceptAnonymousUser(() => contextAction.onPress(closePopup, {...payload, event}), contextAction.isAnonymousAction)}
description={contextAction.getDescription(props.selection, props.isSmallScreenWidth)}
isAnonymousAction={contextAction.isAnonymousAction}
isFocused={focusedIndex === index}
Expand Down
177 changes: 100 additions & 77 deletions src/pages/home/report/ContextMenu/ContextMenuActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import * as Download from '@userActions/Download';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu';
import {hideContextMenu, showContextMenu, showDeleteModal} from './ReportActionContextMenu';

/**
* Gets the HTML version of the message in an action.
Expand Down Expand Up @@ -127,7 +127,7 @@ export default [
{
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.replyInThread',
icon: Expensicons.ChatBubble,
icon: Expensicons.ChatBubbleAdd,
successTextTranslateKey: '',
successIcon: null,
shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => {
Expand All @@ -151,7 +151,76 @@ export default [
},
{
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.subscribeToThread',
textTranslateKey: 'reportActionContextMenu.editAction',
icon: Expensicons.Pencil,
shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport) =>
type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && ReportUtils.canEditReportAction(reportAction) && !isArchivedRoom && !isChronosReport,
onPress: (closePopover, {reportID, reportAction, draftMessage}) => {
if (ReportActionsUtils.isMoneyRequestAction(reportAction)) {
hideContextMenu(false);
const childReportID = lodashGet(reportAction, 'childReportID', 0);
if (!childReportID) {
const thread = ReportUtils.buildTransactionThread(reportAction, reportID);
const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs);
Report.openReport(thread.reportID, userLogins, thread, reportAction.reportActionID);
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID));
return;
}
Report.openReport(childReportID);
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID));
return;
}
const editAction = () => {
if (_.isUndefined(draftMessage)) {
Report.saveReportActionDraft(reportID, reportAction, getActionText(reportAction));
} else {
Report.deleteReportActionDraft(reportID, reportAction);
}
};

if (closePopover) {
// Hide popover, then call editAction
hideContextMenu(false, editAction);
return;
}

// No popover to hide, call editAction immediately
editAction();
},
getDescription: () => {},
},
{
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.markAsUnread',
icon: Expensicons.ChatBubbleUnread,
successIcon: Expensicons.Checkmark,
shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat) =>
type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || (type === CONST.CONTEXT_MENU_TYPES.REPORT && !isUnreadChat),
onPress: (closePopover, {reportAction, reportID}) => {
Report.markCommentAsUnread(reportID, reportAction.created);
if (closePopover) {
hideContextMenu(true, ReportActionComposeFocusManager.focus);
}
},
getDescription: () => {},
},
{
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.markAsRead',
icon: Expensicons.Mail,
successIcon: Expensicons.Checkmark,
shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isUnreadChat,
onPress: (closePopover, {reportID}) => {
Report.readNewestAction(reportID);
if (closePopover) {
hideContextMenu(true, ReportActionComposeFocusManager.focus);
}
},
getDescription: () => {},
},
{
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.joinThread',
icon: Expensicons.Bell,
successTextTranslateKey: '',
successIcon: null,
Expand Down Expand Up @@ -191,7 +260,7 @@ export default [
},
{
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.unsubscribeFromThread',
textTranslateKey: 'reportActionContextMenu.leaveThread',
icon: Expensicons.BellSlash,
successTextTranslateKey: '',
successIcon: null,
Expand Down Expand Up @@ -332,82 +401,11 @@ export default [
getDescription: () => {},
},

{
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.markAsUnread',
icon: Expensicons.Mail,
successIcon: Expensicons.Checkmark,
shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat) =>
type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || (type === CONST.CONTEXT_MENU_TYPES.REPORT && !isUnreadChat),
onPress: (closePopover, {reportAction, reportID}) => {
Report.markCommentAsUnread(reportID, reportAction.created);
if (closePopover) {
hideContextMenu(true, ReportActionComposeFocusManager.focus);
}
},
getDescription: () => {},
},

{
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.markAsRead',
icon: Expensicons.Mail,
successIcon: Expensicons.Checkmark,
shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isUnreadChat,
onPress: (closePopover, {reportID}) => {
Report.readNewestAction(reportID);
if (closePopover) {
hideContextMenu(true, ReportActionComposeFocusManager.focus);
}
},
getDescription: () => {},
},

{
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.editAction',
icon: Expensicons.Pencil,
shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport) =>
type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && ReportUtils.canEditReportAction(reportAction) && !isArchivedRoom && !isChronosReport,
onPress: (closePopover, {reportID, reportAction, draftMessage}) => {
if (ReportActionsUtils.isMoneyRequestAction(reportAction)) {
hideContextMenu(false);
const childReportID = lodashGet(reportAction, 'childReportID', 0);
if (!childReportID) {
const thread = ReportUtils.buildTransactionThread(reportAction, reportID);
const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs);
Report.openReport(thread.reportID, userLogins, thread, reportAction.reportActionID);
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID));
return;
}
Report.openReport(childReportID);
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID));
return;
}
const editAction = () => {
if (_.isUndefined(draftMessage)) {
Report.saveReportActionDraft(reportID, reportAction, getActionText(reportAction));
} else {
Report.deleteReportActionDraft(reportID, reportAction);
}
};

if (closePopover) {
// Hide popover, then call editAction
hideContextMenu(false, editAction);
return;
}

// No popover to hide, call editAction immediately
editAction();
},
getDescription: () => {},
},
{
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.deleteAction',
icon: Expensicons.Trashcan,
shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID) =>
shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) =>
// Until deleting parent threads is supported in FE, we will prevent the user from deleting a thread parent
type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION &&
ReportUtils.canDeleteReportAction(reportAction, reportID) &&
Expand Down Expand Up @@ -456,7 +454,7 @@ export default [
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.flagAsOffensive',
icon: Expensicons.Flag,
shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID) =>
shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) =>
type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION &&
ReportUtils.canFlagReportAction(reportAction, reportID) &&
!isArchivedRoom &&
Expand All @@ -473,4 +471,29 @@ export default [
},
getDescription: () => {},
},
{
isAnonymousAction: true,
textTranslateKey: 'reportActionContextMenu.menu',
icon: Expensicons.ThreeDots,
shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline, isMini) => isMini,
onPress: (closePopover, {reportAction, reportID, event, anchor, selection, draftMessage, checkIfContextMenuActive}) => {
const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction);
const originalReport = ReportUtils.getReport(originalReportID);
showContextMenu(
CONST.CONTEXT_MENU_TYPES.REPORT_ACTION,
event,
selection,
anchor,
reportID,
reportAction.reportActionID,
originalReportID,
draftMessage,
checkIfContextMenuActive,
checkIfContextMenuActive,
ReportUtils.isArchivedRoom(originalReport),
ReportUtils.chatIncludesChronos(originalReport),
);
},
getDescription: () => {},
},
];
Loading