diff --git a/lib/shared/mention-utils.js b/lib/shared/mention-utils.js --- a/lib/shared/mention-utils.js +++ b/lib/shared/mention-utils.js @@ -21,6 +21,19 @@ +end: number, }; +type TypeaheadUserSuggestionItem = { + +type: 'user', + +userInfo: RelativeMemberInfo, +}; + +export type TypeaheadSuggestionItem = TypeaheadUserSuggestionItem; + +export type TypeaheadTooltipActionItem = { + +key: string, + +execute: () => mixed, + +actionButtonContent: SuggestionItemType, +}; + // The simple-markdown package already breaks words out for us, and we are // supposed to only match when the first word of the input matches const markdownMentionRegex: RegExp = new RegExp( @@ -67,7 +80,7 @@ threadMembers: $ReadOnlyArray, viewerID: ?string, usernamePrefix: string, -): $ReadOnlyArray { +): $ReadOnlyArray { const userIDs = userSearchIndex.getSearchResults(usernamePrefix); const usersInThread = threadOtherMembers(threadMembers, viewerID); @@ -75,26 +88,25 @@ .filter(user => usernamePrefix.length === 0 || userIDs.includes(user.id)) .sort((userA, userB) => stringForUserExplicit(userA).localeCompare(stringForUserExplicit(userB)), - ); + ) + .map(userInfo => ({ type: 'user', userInfo })); } function getNewTextAndSelection( textBeforeAtSymbol: string, entireText: string, - usernamePrefix: string, - user: RelativeMemberInfo, + query: string, + suggestionText: string, ): { newText: string, newSelectionStart: number, } { - const totalMatchLength = - textBeforeAtSymbol.length + usernamePrefix.length + 1; // 1 for @ char + const totalMatchLength = textBeforeAtSymbol.length + query.length + 1; // 1 for @ char let newSuffixText = entireText.slice(totalMatchLength); newSuffixText = (newSuffixText[0] !== ' ' ? ' ' : '') + newSuffixText; - const newText = - textBeforeAtSymbol + '@' + stringForUserExplicit(user) + newSuffixText; + const newText = textBeforeAtSymbol + suggestionText + newSuffixText; const newSelectionStart = newText.length - newSuffixText.length + 1; diff --git a/native/chat/chat-input-bar.react.js b/native/chat/chat-input-bar.react.js --- a/native/chat/chat-input-bar.react.js +++ b/native/chat/chat-input-bar.react.js @@ -90,6 +90,7 @@ } from './message-editing-context.react.js'; import type { RemoveEditMode } from './message-list-types.js'; import TypeaheadTooltip from './typeahead-tooltip.react.js'; +import MentionTypeaheadTooltipButton from '../chat/mention-typeahead-tooltip-button.react.js'; import Button from '../components/button.react.js'; // eslint-disable-next-line import/extensions import ClearableTextInput from '../components/clearable-text-input.react'; @@ -129,7 +130,10 @@ import Alert from '../utils/alert.js'; import { runTiming } from '../utils/animation-utils.js'; import { exitEditAlert } from '../utils/edit-messages-utils.js'; -import { nativeTypeaheadRegex } from '../utils/typeahead-utils.js'; +import { + nativeTypeaheadRegex, + mentionTypeaheadTooltipActions, +} from '../utils/typeahead-utils.js'; /* eslint-disable import/no-named-as-default-member */ const { Value, Clock, block, set, cond, neq, sub, interpolateNode, stopClock } = @@ -567,8 +571,10 @@ ); } diff --git a/native/chat/mention-typeahead-tooltip-button.react.js b/native/chat/mention-typeahead-tooltip-button.react.js new file mode 100644 --- /dev/null +++ b/native/chat/mention-typeahead-tooltip-button.react.js @@ -0,0 +1,61 @@ +// @flow + +import * as React from 'react'; +import { Text } from 'react-native'; + +import type { + TypeaheadTooltipActionItem, + TypeaheadSuggestionItem, +} from 'lib/shared/mention-utils.js'; + +import UserAvatar from '../avatars/user-avatar.react.js'; +import Button from '../components/button.react.js'; +import { useStyles } from '../themes/colors.js'; + +type Props = { + +item: TypeaheadTooltipActionItem, +}; +function MentionTypeaheadTooltipButton(props: Props): React.Node { + const { item } = props; + const styles = useStyles(unboundStyles); + + let avatarComponent = null; + let typeaheadTooltipButtonText = null; + if (item.actionButtonContent.type === 'user') { + avatarComponent = ( + + ); + typeaheadTooltipButtonText = item.actionButtonContent.userInfo.username; + } + + return ( + + ); +} + +const unboundStyles = { + button: { + alignItems: 'center', + flexDirection: 'row', + innerHeight: 24, + padding: 8, + color: 'typeaheadTooltipText', + }, + buttonLabel: { + color: 'white', + fontSize: 16, + fontWeight: '400', + marginLeft: 8, + }, +}; + +export default MentionTypeaheadTooltipButton; diff --git a/native/chat/typeahead-tooltip.react.js b/native/chat/typeahead-tooltip.react.js --- a/native/chat/typeahead-tooltip.react.js +++ b/native/chat/typeahead-tooltip.react.js @@ -1,67 +1,59 @@ // @flow import * as React from 'react'; -import { Platform, Text } from 'react-native'; +import { Platform } from 'react-native'; import { PanGestureHandler, FlatList } from 'react-native-gesture-handler'; import { type TypeaheadMatchedStrings, type Selection, - getNewTextAndSelection, + type TypeaheadTooltipActionItem, } from 'lib/shared/mention-utils.js'; -import type { RelativeMemberInfo } from 'lib/types/thread-types.js'; -import UserAvatar from '../avatars/user-avatar.react.js'; -import Button from '../components/button.react.js'; import { useStyles } from '../themes/colors.js'; +import type { + TypeaheadTooltipActionsParams, + TypeaheadTooltipButtonComponentType, +} from '../utils/typeahead-utils.js'; -export type TypeaheadTooltipProps = { +export type TypeaheadTooltipProps = { +text: string, +matchedStrings: TypeaheadMatchedStrings, - +suggestedUsers: $ReadOnlyArray, + +suggestions: $ReadOnlyArray, +focusAndUpdateTextAndSelection: (text: string, selection: Selection) => void, + +typeaheadTooltipActionsGetter: ( + TypeaheadTooltipActionsParams, + ) => $ReadOnlyArray>, + +TypeaheadTooltipButtonComponent: TypeaheadTooltipButtonComponentType, }; -function TypeaheadTooltip(props: TypeaheadTooltipProps): React.Node { +function TypeaheadTooltip( + props: TypeaheadTooltipProps, +): React.Node { const { text, matchedStrings, - suggestedUsers, + suggestions, focusAndUpdateTextAndSelection, + TypeaheadTooltipButtonComponent, + typeaheadTooltipActionsGetter, } = props; const { textBeforeAtSymbol, query } = matchedStrings; const styles = useStyles(unboundStyles); - - const renderTypeaheadButton = React.useCallback( - ({ item }: { item: RelativeMemberInfo, ... }) => { - const onPress = () => { - const { newText, newSelectionStart } = getNewTextAndSelection( - textBeforeAtSymbol, - text, - query, - item, - ); - - focusAndUpdateTextAndSelection(newText, { - start: newSelectionStart, - end: newSelectionStart, - }); - }; - - return ( - - ); - }, + const actions = React.useMemo( + () => + typeaheadTooltipActionsGetter({ + suggestions, + textBeforeAtSymbol, + text, + query, + focusAndUpdateTextAndSelection, + }), [ - styles.button, - styles.buttonLabel, + typeaheadTooltipActionsGetter, + suggestions, textBeforeAtSymbol, text, query, @@ -69,6 +61,16 @@ ], ); + const renderTypeaheadButton = React.useCallback( + ({ + item, + }: { + item: TypeaheadTooltipActionItem, + ... + }) => , + [], + ); + // This is a hack that was introduced due to a buggy behavior of a // absolutely positioned FlatList on Android. @@ -95,17 +97,12 @@ ), - [ - renderTypeaheadButton, - styles.container, - styles.contentContainer, - suggestedUsers, - ], + [actions, renderTypeaheadButton, styles.container, styles.contentContainer], ); const listWithConditionalHandler = React.useMemo(() => { @@ -134,19 +131,6 @@ contentContainer: { padding: 8, }, - button: { - alignItems: 'center', - flexDirection: 'row', - innerHeight: 24, - padding: 8, - color: 'typeaheadTooltipText', - }, - buttonLabel: { - color: 'white', - fontSize: 16, - fontWeight: '400', - marginLeft: 8, - }, }; export default TypeaheadTooltip; diff --git a/native/utils/typeahead-utils.js b/native/utils/typeahead-utils.js --- a/native/utils/typeahead-utils.js +++ b/native/utils/typeahead-utils.js @@ -1,6 +1,15 @@ // @flow +import * as React from 'react'; + import { oldValidUsernameRegexString } from 'lib/shared/account-utils.js'; +import { + getNewTextAndSelection, + type Selection, + type TypeaheadTooltipActionItem, + type TypeaheadSuggestionItem, +} from 'lib/shared/mention-utils.js'; +import { stringForUserExplicit } from 'lib/shared/user-utils.js'; // Native regex is a little bit different than web one as // there are no named capturing groups yet on native. @@ -8,4 +17,58 @@ `((^(.|\n)*\\s+)|^)@(${oldValidUsernameRegexString})?$`, ); -export { nativeTypeaheadRegex }; +export type TypeaheadTooltipActionsParams = { + +suggestions: $ReadOnlyArray, + +textBeforeAtSymbol: string, + +text: string, + +query: string, + +focusAndUpdateTextAndSelection: (text: string, selection: Selection) => void, +}; +function mentionTypeaheadTooltipActions({ + suggestions, + textBeforeAtSymbol, + text, + query, + focusAndUpdateTextAndSelection, +}: TypeaheadTooltipActionsParams): $ReadOnlyArray< + TypeaheadTooltipActionItem, +> { + const actions = []; + for (const suggestion of suggestions) { + if (suggestion.type === 'user') { + const { userInfo } = suggestion; + const resolvedUsername = stringForUserExplicit(userInfo); + if (resolvedUsername === 'anonymous') { + continue; + } + const mentionText = `@${resolvedUsername}`; + actions.push({ + key: userInfo.id, + execute: () => { + const { newText, newSelectionStart } = getNewTextAndSelection( + textBeforeAtSymbol, + text, + query, + mentionText, + ); + focusAndUpdateTextAndSelection(newText, { + start: newSelectionStart, + end: newSelectionStart, + }); + }, + actionButtonContent: { + type: 'user', + userInfo, + }, + }); + } + } + return actions; +} + +export type TypeaheadTooltipButtonComponentType = + React.ComponentType<{ + +item: TypeaheadTooltipActionItem, + }>; + +export { nativeTypeaheadRegex, mentionTypeaheadTooltipActions }; diff --git a/web/chat/chat-input-bar.react.js b/web/chat/chat-input-bar.react.js --- a/web/chat/chat-input-bar.react.js +++ b/web/chat/chat-input-bar.react.js @@ -631,7 +631,7 @@ props.inputState.typeaheadState.frozenMentionsCandidates, viewerID, typeaheadMatchedStrings.query, - ); + ).map(suggestion => suggestion.userInfo); }, [ userSearchIndex, props.inputState.typeaheadState.frozenMentionsCandidates, diff --git a/web/utils/typeahead-utils.js b/web/utils/typeahead-utils.js --- a/web/utils/typeahead-utils.js +++ b/web/utils/typeahead-utils.js @@ -107,7 +107,7 @@ textBeforeAtSymbol, inputStateDraft, query, - suggestedUser, + stringForUserExplicit(suggestedUser), ); inputStateSetDraft(newText);