Skip to content

Commit

Permalink
Merge pull request Expensify#35226 from dukenv0307/fix/34442
Browse files Browse the repository at this point in the history
Implement suggestion for edit composer
  • Loading branch information
stitesExpensify authored Apr 23, 2024
2 parents 63affdb + b820982 commit d296ef8
Show file tree
Hide file tree
Showing 18 changed files with 340 additions and 21 deletions.
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {WindowDimensionsProvider} from './components/withWindowDimensions';
import Expensify from './Expensify';
import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop';
import OnyxUpdateManager from './libs/actions/OnyxUpdateManager';
import {SuggestionsContextProvider} from './pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext';
import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext';
import type {Route} from './ROUTES';

Expand Down Expand Up @@ -79,6 +80,7 @@ function App({url}: AppProps) {
ActiveElementRoleProvider,
ActiveWorkspaceContextProvider,
PlaybackContextProvider,
SuggestionsContextProvider,
FullScreenContextProvider,
VolumeContextProvider,
VideoPopoverMenuContextProvider,
Expand Down
2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type OnboardingPurposeType = ValueOf<typeof onboardingChoices>;
const CONST = {
MERGED_ACCOUNT_PREFIX: 'MERGED_',
DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL],
DEFAULT_COMPOSER_PORTAL_HOST_NAME: 'suggestions_0',

// Note: Group and Self-DM excluded as these are not tied to a Workspace
WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT],
Expand Down Expand Up @@ -1171,6 +1172,7 @@ const CONST = {
EMOJI_PICKER_HEADER_HEIGHT: 32,
RECIPIENT_LOCAL_TIME_HEIGHT: 25,
AUTO_COMPLETE_SUGGESTER: {
EDIT_SUGGESTER_PADDING: 8,
SUGGESTER_PADDING: 6,
SUGGESTER_INNER_PADDING: 8,
SUGGESTION_ROW_HEIGHT: 40,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {useSuggestionsContext} from '@pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import viewForwardedRef from '@src/types/utils/viewForwardedRef';
Expand Down Expand Up @@ -39,6 +40,7 @@ function BaseAutoCompleteSuggestions<TSuggestion>(
suggestions,
isSuggestionPickerLarge,
keyExtractor,
shouldBeDisplayedBelowParentContainer = false,
}: AutoCompleteSuggestionsProps<TSuggestion>,
ref: ForwardedRef<View | HTMLDivElement>,
) {
Expand All @@ -47,6 +49,7 @@ function BaseAutoCompleteSuggestions<TSuggestion>(
const StyleUtils = useStyleUtils();
const rowHeight = useSharedValue(0);
const scrollRef = useRef<FlashList<TSuggestion>>(null);
const {activeID} = useSuggestionsContext();
/**
* Render a suggestion menu item component.
*/
Expand All @@ -68,7 +71,7 @@ function BaseAutoCompleteSuggestions<TSuggestion>(
);

const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length;
const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value));
const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value, shouldBeDisplayedBelowParentContainer, Boolean(activeID)));
const estimatedListSize = useMemo(
() => ({
height: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length,
Expand Down
5 changes: 4 additions & 1 deletion src/components/AutoCompleteSuggestions/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import {Portal} from '@gorhom/portal';
import React from 'react';
import {useSuggestionsContext} from '@pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext';
import CONST from '@src/CONST';
import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions';
import type {AutoCompleteSuggestionsProps} from './types';

function AutoCompleteSuggestions<TSuggestion>({measureParentContainer, ...props}: AutoCompleteSuggestionsProps<TSuggestion>) {
const {activeID} = useSuggestionsContext();
return (
<Portal hostName="suggestions">
<Portal hostName={activeID ? `suggestions_${activeID}` : CONST.DEFAULT_COMPOSER_PORTAL_HOST_NAME}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<BaseAutoCompleteSuggestions<TSuggestion> {...props} />
</Portal>
Expand Down
13 changes: 11 additions & 2 deletions src/components/AutoCompleteSuggestions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {View} from 'react-native';
import useStyleUtils from '@hooks/useStyleUtils';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import {measureHeightOfSuggestionsContainer} from '@libs/SuggestionUtils';
import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions';
import type {AutoCompleteSuggestionsProps} from './types';

Expand All @@ -18,11 +19,13 @@ function AutoCompleteSuggestions<TSuggestion>({measureParentContainer = () => {}
const StyleUtils = useStyleUtils();
const containerRef = React.useRef<HTMLDivElement>(null);
const {windowHeight, windowWidth} = useWindowDimensions();
const suggestionsContainerHeight = measureHeightOfSuggestionsContainer(props.suggestions.length, props.isSuggestionPickerLarge);
const [{width, left, bottom}, setContainerState] = React.useState({
width: 0,
left: 0,
bottom: 0,
});
const [shouldShowBelowContainer, setShouldShowBelowContainer] = React.useState(false);
React.useEffect(() => {
const container = containerRef.current;
if (!container) {
Expand All @@ -41,13 +44,19 @@ function AutoCompleteSuggestions<TSuggestion>({measureParentContainer = () => {}
if (!measureParentContainer) {
return;
}
measureParentContainer((x, y, w) => setContainerState({left: x, bottom: windowHeight - y, width: w}));
}, [measureParentContainer, windowHeight, windowWidth]);

measureParentContainer((x, y, w, h) => {
const currentBottom = y < suggestionsContainerHeight ? windowHeight - y - suggestionsContainerHeight - h : windowHeight - y;
setShouldShowBelowContainer(y < suggestionsContainerHeight);
setContainerState({left: x, bottom: currentBottom, width: w});
});
}, [measureParentContainer, windowHeight, windowWidth, suggestionsContainerHeight]);

const componentToRender = (
<BaseAutoCompleteSuggestions<TSuggestion>
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
shouldBeDisplayedBelowParentContainer={shouldShowBelowContainer}
ref={containerRef}
/>
);
Expand Down
5 changes: 4 additions & 1 deletion src/components/AutoCompleteSuggestions/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {ReactElement} from 'react';

type MeasureParentContainerCallback = (x: number, y: number, width: number) => void;
type MeasureParentContainerCallback = (x: number, y: number, width: number, height: number) => void;

type RenderSuggestionMenuItemProps<TSuggestion> = {
item: TSuggestion;
Expand Down Expand Up @@ -33,6 +33,9 @@ type AutoCompleteSuggestionsProps<TSuggestion> = {

/** Meaures the parent container's position and dimensions. */
measureParentContainer?: (callback: MeasureParentContainerCallback) => void;

/** Whether suggestion should be displayed below the parent container or not */
shouldBeDisplayedBelowParentContainer?: boolean;
};

export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps};
2 changes: 1 addition & 1 deletion src/components/EmojiSuggestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import getStyledTextArray from '@libs/GetStyledTextArray';
import AutoCompleteSuggestions from './AutoCompleteSuggestions';
import Text from './Text';

type MeasureParentContainerCallback = (x: number, y: number, width: number) => void;
type MeasureParentContainerCallback = (x: number, y: number, width: number, height: number) => void;

type EmojiSuggestionsProps = {
/** The index of the highlighted emoji */
Expand Down
23 changes: 22 additions & 1 deletion src/libs/SuggestionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,25 @@ function hasEnoughSpaceForLargeSuggestionMenu(listHeight: number, composerHeight
return availableHeight > menuHeight;
}

export {trimLeadingSpace, hasEnoughSpaceForLargeSuggestionMenu};
const measureHeightOfSuggestionsContainer = (numRows: number, isSuggestionsPickerLarge: boolean): number => {
// Autocomplete suggestions has inner padding 8px and border-width 1px
const borderAndPadding = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_INNER_PADDING + 2;
let suggestionsHeight = 0;

if (isSuggestionsPickerLarge) {
if (numRows > CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER) {
// On large screens, if there are more than 5 suggestions, we display a scrollable window with a height of 5 items, indicating that there are more items available
suggestionsHeight = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT;
} else {
suggestionsHeight = numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT;
}
} else if (numRows > 2) {
// On small screens, we display a scrollable window with a height of 2.5 items, indicating that there are more items available beyond what is currently visible
suggestionsHeight = CONST.AUTO_COMPLETE_SUGGESTER.SMALL_CONTAINER_HEIGHT_FACTOR * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT;
} else {
suggestionsHeight = numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT;
}
return suggestionsHeight + borderAndPadding;
};

export {trimLeadingSpace, hasEnoughSpaceForLargeSuggestionMenu, measureHeightOfSuggestionsContainer};
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type {Dispatch, ForwardedRef, RefObject, SetStateAction} from 'react';
import React, {useState} from 'react';
import type {MeasureInWindowOnSuccessCallback, TextInput} from 'react-native';
import Composer from '@components/Composer';
import type {ComposerProps} from '@components/Composer/types';
import type {SuggestionsRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose';
import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions';

type Selection = {
start: number;
end: number;
};

type ComposerWithSuggestionsEditProps = ComposerProps & {
setValue: Dispatch<SetStateAction<string>>;
setSelection: Dispatch<SetStateAction<Selection>>;
resetKeyboardInput: () => void;
isComposerFocused: boolean;
suggestionsRef: RefObject<SuggestionsRef>;
updateDraft: (newValue: string) => void;
measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void;
value: string;
selection: Selection;
isGroupPolicyReport: boolean;
};

function ComposerWithSuggestionsEdit(
{
value,
maxLines = -1,
onKeyPress = () => {},
style,
onSelectionChange = () => {},
selection = {
start: 0,
end: 0,
},
onBlur = () => {},
onFocus = () => {},
onChangeText = () => {},
setValue = () => {},
setSelection = () => {},
resetKeyboardInput = () => {},
isComposerFocused,
suggestionsRef,
updateDraft,
measureParentContainer,
id = undefined,
isGroupPolicyReport,
}: ComposerWithSuggestionsEditProps,
ref: ForwardedRef<TextInput>,
) {
const [composerHeight, setComposerHeight] = useState(0);

return (
<>
<Composer
multiline
ref={ref}
id={id}
onChangeText={onChangeText} // Debounced saveDraftComment
onKeyPress={onKeyPress}
value={value}
maxLines={maxLines} // This is the same that slack has
style={style}
onFocus={onFocus}
onBlur={onBlur}
selection={selection}
onSelectionChange={onSelectionChange}
onLayout={(e) => {
const composerLayoutHeight = e.nativeEvent.layout.height;
if (composerHeight === composerLayoutHeight) {
return;
}
setComposerHeight(composerLayoutHeight);
}}
/>

<Suggestions
ref={suggestionsRef}
isComposerFullSize={false}
isComposerFocused={isComposerFocused}
updateComment={updateDraft}
composerHeight={composerHeight}
measureParentContainer={measureParentContainer}
isAutoSuggestionPickerLarge
value={value}
setValue={setValue}
selection={selection}
setSelection={setSelection}
resetKeyboardInput={resetKeyboardInput}
isGroupPolicyReport={isGroupPolicyReport}
/>
</>
);
}

export default React.forwardRef(ComposerWithSuggestionsEdit);
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type {MutableRefObject, ReactNode} from 'react';
import React, {createContext, useCallback, useContext, useMemo, useRef, useState} from 'react';
import type {SuggestionsRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose';

type SuggestionsContextProviderProps = {
children?: ReactNode;
};

type SuggestionsContextProps = {
activeID: string | null;
currentActiveSuggestionsRef: MutableRefObject<SuggestionsRef | null>;
updateCurrentActiveSuggestionsRef: (ref: SuggestionsRef | null, id: string) => void;
clearActiveSuggestionsRef: () => void;
isActiveSuggestions: (id: string) => boolean;
};

const SuggestionsContext = createContext<SuggestionsContextProps>({
activeID: null,
currentActiveSuggestionsRef: {current: null},
updateCurrentActiveSuggestionsRef: () => {},
clearActiveSuggestionsRef: () => {},
isActiveSuggestions: () => false,
});

function SuggestionsContextProvider({children}: SuggestionsContextProviderProps) {
const currentActiveSuggestionsRef = useRef<SuggestionsRef | null>(null);
const [activeID, setActiveID] = useState<string | null>(null);

const updateCurrentActiveSuggestionsRef = useCallback((ref: SuggestionsRef | null, id: string) => {
currentActiveSuggestionsRef.current = ref;
setActiveID(id);
}, []);

const clearActiveSuggestionsRef = useCallback(() => {
currentActiveSuggestionsRef.current = null;
setActiveID(null);
}, []);

const isActiveSuggestions = useCallback((id: string) => id === activeID, [activeID]);

const contextValue = useMemo(
() => ({activeID, currentActiveSuggestionsRef, updateCurrentActiveSuggestionsRef, clearActiveSuggestionsRef, isActiveSuggestions}),
[activeID, currentActiveSuggestionsRef, updateCurrentActiveSuggestionsRef, clearActiveSuggestionsRef, isActiveSuggestions],
);

return <SuggestionsContext.Provider value={contextValue}>{children}</SuggestionsContext.Provider>;
}

function useSuggestionsContext() {
const context = useContext(SuggestionsContext);
return context;
}

SuggestionsContextProvider.displayName = 'SuggestionsContextProvider';

export {SuggestionsContextProvider, useSuggestionsContext};
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type SuggestionsRef = {
updateShouldShowSuggestionMenuToFalse: (shouldShowSuggestionMenu?: boolean) => void;
setShouldBlockSuggestionCalc: (shouldBlock: boolean) => void;
getSuggestions: () => Mention[] | Emoji[];
updateShouldShowSuggestionMenuAfterScrolling: () => void;
};

type ReportActionComposeOnyxProps = {
Expand Down Expand Up @@ -378,7 +379,7 @@ function ReportActionCompose({
{shouldShowReportRecipientLocalTime && hasReportRecipient && <ParticipantLocalTime participant={reportRecipient} />}
</OfflineWithFeedback>
<View style={isComposerFullSize ? styles.flex1 : {}}>
<PortalHost name="suggestions" />
<PortalHost name={CONST.DEFAULT_COMPOSER_PORTAL_HOST_NAME} />
<OfflineWithFeedback
pendingAction={pendingAction}
style={isComposerFullSize ? styles.chatItemFullComposeRow : {}}
Expand Down
15 changes: 14 additions & 1 deletion src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ function SuggestionEmoji(
});
}, []);

const updateShouldShowSuggestionMenuAfterScrolling = useCallback(() => {
setSuggestionValues((prevState) => ({...prevState, shouldShowSuggestionMenu: !!prevState.suggestedEmojis.length}));
}, []);

/**
* Listens for keyboard shortcuts and applies the action
*/
Expand Down Expand Up @@ -215,8 +219,17 @@ function SuggestionEmoji(
setShouldBlockSuggestionCalc,
updateShouldShowSuggestionMenuToFalse,
getSuggestions,
updateShouldShowSuggestionMenuAfterScrolling,
}),
[onSelectionChange, resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse, getSuggestions],
[
onSelectionChange,
resetSuggestions,
setShouldBlockSuggestionCalc,
triggerHotkeyActions,
updateShouldShowSuggestionMenuToFalse,
getSuggestions,
updateShouldShowSuggestionMenuAfterScrolling,
],
);

if (!isEmojiSuggestionsMenuVisible) {
Expand Down
Loading

0 comments on commit d296ef8

Please sign in to comment.