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

Implement suggestion for edit composer #35226

Merged
merged 48 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
99c7092
test
dukenv0307 Jan 15, 2024
dee95c9
implement suggestion for edit composer
dukenv0307 Jan 15, 2024
d40e5cc
Merge branch 'main' into fix/34442
dukenv0307 Jan 25, 2024
0de2686
implement suggestion for edit composer
dukenv0307 Jan 25, 2024
a1ef1bf
Merge branch 'main' into fix/34442
dukenv0307 Jan 26, 2024
0273240
display suggestion below when composer at the top of the screen
dukenv0307 Jan 26, 2024
31bf412
fix edge case
dukenv0307 Jan 26, 2024
e5eb167
Merge branch 'main' into fix/34442
dukenv0307 Jan 29, 2024
ee43388
create global ref
dukenv0307 Jan 29, 2024
4858707
Merge branch 'main' into fix/34442
dukenv0307 Feb 6, 2024
bd0ccb7
merge main
dukenv0307 Feb 6, 2024
5f4c43a
merge main
dukenv0307 Feb 19, 2024
f11b8ed
fix lint
dukenv0307 Feb 19, 2024
a7bcde6
update suggestion
dukenv0307 Feb 19, 2024
be5faa2
rename variable
dukenv0307 Feb 19, 2024
c4094f3
merge main
dukenv0307 Feb 26, 2024
eb09849
move portal of suggestion to the correct place
dukenv0307 Feb 26, 2024
a663612
merge main
dukenv0307 Feb 29, 2024
0b5ea25
Merge branch 'main' into fix/34442
dukenv0307 Mar 1, 2024
930eb47
remove global ref and create suggestion context
dukenv0307 Mar 4, 2024
ec83df8
Merge branch 'main' into fix/34442
dukenv0307 Mar 6, 2024
5e5cd88
merge main
dukenv0307 Mar 11, 2024
01c63b8
merge main
dukenv0307 Mar 13, 2024
cc1810a
Merge branch 'main' into fix/34442
dukenv0307 Mar 15, 2024
8dcd362
fix the suggestion padding for edit composer
dukenv0307 Mar 15, 2024
05f316f
Merge branch 'main' into fix/34442
dukenv0307 Mar 19, 2024
07db2ab
update variable name
dukenv0307 Mar 19, 2024
d511cb8
merge main
dukenv0307 Mar 20, 2024
c093a99
fix portal on native
dukenv0307 Mar 20, 2024
616c3c7
rename
dukenv0307 Mar 20, 2024
8caeee3
resolve conflict
dukenv0307 Mar 22, 2024
8c961f3
resolve conflict
dukenv0307 Mar 25, 2024
a2065fd
resolve conflict
dukenv0307 Mar 25, 2024
be61ac7
merge main
dukenv0307 Mar 26, 2024
4ad4341
Merge branch 'main' into fix/34442
dukenv0307 Mar 27, 2024
265ced8
resolve conflict
dukenv0307 Apr 1, 2024
c7defeb
merge main
dukenv0307 Apr 1, 2024
d5d7f24
re-open suggestion menu after scrolling
dukenv0307 Apr 1, 2024
10cedd0
Merge branch 'main' into fix/34442
dukenv0307 Apr 5, 2024
3ff18ff
fix suggestion open after scrolling
dukenv0307 Apr 5, 2024
d923e85
Merge branch 'main' into fix/34442
dukenv0307 Apr 10, 2024
0e7eb4f
create new const for suggestion portal host name
dukenv0307 Apr 10, 2024
f57ca56
Merge branch 'main' into fix/34442
dukenv0307 Apr 12, 2024
d734b75
Merge branch 'main' into fix/34442
dukenv0307 Apr 19, 2024
b153d07
Merge branch 'main' into fix/34442
dukenv0307 Apr 22, 2024
c95b4aa
add explain comment
dukenv0307 Apr 22, 2024
cd0f980
add isGroupPolicyReport prop
dukenv0307 Apr 22, 2024
b820982
fix ts
dukenv0307 Apr 22, 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
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 @@ -1165,6 +1166,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
Loading