Skip to content

Commit

Permalink
Merge pull request #50047 from software-mansion-labs/@Skalakid/inline…
Browse files Browse the repository at this point in the history
…-images-feature-v2

Add inline images preview to the Live Markdown Input on the web
  • Loading branch information
thienlnam authored Oct 11, 2024
2 parents 2f1b6f1 + 70fa23d commit 3c163df
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 94 deletions.
8 changes: 4 additions & 4 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2360,7 +2360,7 @@ PODS:
- RNGoogleSignin (10.0.1):
- GoogleSignIn (~> 7.0)
- React-Core
- RNLiveMarkdown (0.1.143):
- RNLiveMarkdown (0.1.164):
- DoubleConversion
- glog
- hermes-engine
Expand All @@ -2380,9 +2380,9 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNLiveMarkdown/common (= 0.1.143)
- RNLiveMarkdown/newarch (= 0.1.164)
- Yoga
- RNLiveMarkdown/common (0.1.143):
- RNLiveMarkdown/newarch (0.1.164):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -3229,7 +3229,7 @@ SPEC CHECKSUMS:
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 8781e2529230a1bc3ea8d75e5c3cd071b6c6aed7
RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0
RNLiveMarkdown: e44918843c2638692348f39eafc275698baf0444
RNLiveMarkdown: b2bd97a6f1206be16cf6536c092fe39f986aca34
RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4
RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
},
"dependencies": {
"@dotlottie/react-player": "^1.6.3",
"@expensify/react-native-live-markdown": "0.1.143",
"@expensify/react-native-live-markdown": "0.1.164",
"@expo/metro-runtime": "~3.2.3",
"@firebase/app": "^0.10.10",
"@firebase/performance": "^0.6.8",
Expand Down
129 changes: 44 additions & 85 deletions src/components/Composer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
import lodashDebounce from 'lodash/debounce';
import type {BaseSyntheticEvent, ForwardedRef} from 'react';
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {flushSync} from 'react-dom';
// eslint-disable-next-line no-restricted-imports
import type {NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData, ViewStyle} from 'react-native';
import {DeviceEventEmitter, StyleSheet, View} from 'react-native';
import type {NativeSyntheticEvent, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData} from 'react-native';
import {DeviceEventEmitter, StyleSheet} from 'react-native';
import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
import RNMarkdownTextInput from '@components/RNMarkdownTextInput';
import Text from '@components/Text';
import useHtmlPaste from '@hooks/useHtmlPaste';
import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible';
import useMarkdownStyle from '@hooks/useMarkdownStyle';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
import * as Browser from '@libs/Browser';
import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable';
import * as EmojiUtils from '@libs/EmojiUtils';
Expand All @@ -23,31 +22,9 @@ import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposit
import CONST from '@src/CONST';
import type {ComposerProps} from './types';

/**
* Retrieves the characters from the specified cursor position up to the next space or new line.
*
* @param inputString - The input string.
* @param cursorPosition - The position of the cursor within the input string.
* @returns - The substring from the cursor position up to the next space or new line.
* If no space or new line is found, returns the substring from the cursor position to the end of the input string.
*/
const getNextChars = (inputString: string, cursorPosition: number): string => {
// Get the substring starting from the cursor position
const subString = inputString.substring(cursorPosition);

// Find the index of the next space or new line character
const spaceIndex = subString.search(/[ \n]/);

if (spaceIndex === -1) {
return subString;
}

// If there is a space or new line, return the substring up to the space or new line
return subString.substring(0, spaceIndex);
};

const excludeNoStyles: Array<keyof MarkdownStyle> = [];
const excludeReportMentionStyle: Array<keyof MarkdownStyle> = ['mentionReport'];
const imagePreviewAuthRequiredURLs = [CONST.EXPENSIFY_URL, CONST.STAGING_EXPENSIFY_URL];

// Enable Markdown parsing.
// On web we like to have the Text Input field always focused so the user can easily type a new chat
Expand Down Expand Up @@ -83,7 +60,6 @@ function Composer(
const styles = useThemeStyles();
const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);
const StyleUtils = useStyleUtils();
const textRef = useRef<HTMLElement & RNText>(null);
const textInput = useRef<AnimatedMarkdownTextInputRef | null>(null);
const [selection, setSelection] = useState<
| {
Expand All @@ -97,9 +73,6 @@ function Composer(
start: selectionProp.start,
end: selectionProp.end,
});
const [caretContent, setCaretContent] = useState('');
const [valueBeforeCaret, setValueBeforeCaret] = useState('');
const [textInputWidth, setTextInputWidth] = useState<ViewStyle['width']>('');
const [isRendered, setIsRendered] = useState(false);
const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? '');
const [prevScroll, setPrevScroll] = useState<number | undefined>();
Expand All @@ -118,17 +91,25 @@ function Composer(
*/
const addCursorPositionToSelectionChange = (event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
const webEvent = event as BaseSyntheticEvent<TextInputSelectionChangeEventData>;
if (shouldCalculateCaretPosition && isRendered) {
// we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state
flushSync(() => {
setValueBeforeCaret((webEvent.target as HTMLInputElement).value.slice(0, webEvent.nativeEvent.selection.start));
setCaretContent(getNextChars(value ?? '', webEvent.nativeEvent.selection.start));
});
const sel = window.getSelection();
if (shouldCalculateCaretPosition && isRendered && sel) {
const range = sel.getRangeAt(0).cloneRange();
range.collapse(true);
const rect = range.getClientRects()[0] || range.startContainer.parentElement?.getClientRects()[0];
const containerRect = textInput.current?.getBoundingClientRect();

let x = 0;
let y = 0;
if (rect && containerRect) {
x = rect.left - containerRect.left;
y = rect.top - containerRect.top + (textInput?.current?.scrollTop ?? 0) - rect.height / 2;
}

const selectionValue = {
start: webEvent.nativeEvent.selection.start,
end: webEvent.nativeEvent.selection.end,
positionX: (textRef.current?.offsetLeft ?? 0) - CONST.SPACE_CHARACTER_WIDTH,
positionY: textRef.current?.offsetTop,
positionX: x - CONST.SPACE_CHARACTER_WIDTH,
positionY: y,
};

onSelectionChange({
Expand Down Expand Up @@ -335,26 +316,6 @@ function Composer(
[onKeyPress],
);

const renderElementForCaretPosition = (
<View
style={{
position: 'absolute',
zIndex: -1,
opacity: 0,
}}
>
<Text style={[StyleSheet.flatten([style, styles.noSelect]), StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), {maxWidth: textInputWidth}]}>
{`${valueBeforeCaret} `}
<Text
numberOfLines={1}
ref={textRef}
>
{`${caretContent}`}
</Text>
</Text>
</View>
);

const scrollStyleMemo = useMemo(() => {
if (shouldContainScroll) {
return isScrollBarVisible ? [styles.overflowScroll, styles.overscrollBehaviorContain] : styles.overflowHidden;
Expand All @@ -377,32 +338,30 @@ function Composer(
);

return (
<>
<RNMarkdownTextInput
id={CONST.COMPOSER.NATIVE_ID}
autoComplete="off"
autoCorrect={!Browser.isMobileSafari()}
placeholderTextColor={theme.placeholderText}
ref={(el) => (textInput.current = el)}
selection={selection}
style={[inputStyleMemo]}
markdownStyle={markdownStyle}
value={value}
defaultValue={defaultValue}
autoFocus={autoFocus}
inputMode={showSoftInputOnFocus ? 'text' : 'none'}
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...props}
onSelectionChange={addCursorPositionToSelectionChange}
onContentSizeChange={(e) => {
setTextInputWidth(`${e.nativeEvent.contentSize.width}px`);
updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles);
}}
disabled={isDisabled}
onKeyPress={handleKeyPress}
/>
{shouldCalculateCaretPosition && renderElementForCaretPosition}
</>
<RNMarkdownTextInput
id={CONST.COMPOSER.NATIVE_ID}
autoComplete="off"
autoCorrect={!Browser.isMobileSafari()}
placeholderTextColor={theme.placeholderText}
ref={(el) => (textInput.current = el)}
selection={selection}
style={[inputStyleMemo]}
markdownStyle={markdownStyle}
value={value}
defaultValue={defaultValue}
autoFocus={autoFocus}
inputMode={showSoftInputOnFocus ? 'text' : 'none'}
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...props}
onSelectionChange={addCursorPositionToSelectionChange}
onContentSizeChange={(e) => {
updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles);
}}
disabled={isDisabled}
onKeyPress={handleKeyPress}
addAuthTokenToImageURLCallback={addEncryptedAuthTokenToURL}
imagePreviewAuthRequiredURLs={imagePreviewAuthRequiredURLs}
/>
);
}

Expand Down
13 changes: 13 additions & 0 deletions src/hooks/useMarkdownStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ function useMarkdownStyle(message: string | null = null, excludeStyles: Array<ke
color: theme.mentionText,
backgroundColor: theme.mentionBG,
},
inlineImage: {
minWidth: variables.inlineImagePreviewMinSize,
minHeight: variables.inlineImagePreviewMinSize,
maxWidth: variables.inlineImagePreviewMaxSize,
maxHeight: variables.inlineImagePreviewMaxSize,
borderRadius: variables.componentBorderRadius,
marginTop: 4,
},
loadingIndicator: {
primaryColor: theme.spinner,
secondaryColor: `${theme.spinner}33`,
},
loadingIndicatorContainer: {},
};

if (excludeStyles.length) {
Expand Down
3 changes: 3 additions & 0 deletions src/styles/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,9 @@ export default {
composerTooltipShiftVertical: -10,
gbrTooltipShiftHorizontal: -20,

inlineImagePreviewMinSize: 64,
inlineImagePreviewMaxSize: 148,

minimalTopBarOffset: -26,
searchHeaderHeight: 80,
searchListContentMarginTop: 116,
Expand Down

0 comments on commit 3c163df

Please sign in to comment.