Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3390692
D8897.id31500.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
13 KB
Referenced Files
None
Subscribers
None
D8897.id31500.diff
View Options
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<SuggestionItemType> = {
+ +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<RelativeMemberInfo>,
viewerID: ?string,
usernamePrefix: string,
-): $ReadOnlyArray<RelativeMemberInfo> {
+): $ReadOnlyArray<TypeaheadUserSuggestionItem> {
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 @@
<TypeaheadTooltip
text={this.state.text}
matchedStrings={typeaheadMatchedStrings}
- suggestedUsers={suggestedUsers}
+ suggestions={suggestedUsers}
focusAndUpdateTextAndSelection={this.focusAndUpdateTextAndSelection}
+ typeaheadTooltipActionsGetter={mentionTypeaheadTooltipActions}
+ TypeaheadTooltipButtonComponent={MentionTypeaheadTooltipButton}
/>
);
}
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<TypeaheadSuggestionItem>,
+};
+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 = (
+ <UserAvatar size="small" userID={item.actionButtonContent.userInfo.id} />
+ );
+ typeaheadTooltipButtonText = item.actionButtonContent.userInfo.username;
+ }
+
+ return (
+ <Button
+ onPress={item.execute}
+ style={styles.button}
+ iosActiveOpacity={0.85}
+ >
+ {avatarComponent}
+ <Text style={styles.buttonLabel} numberOfLines={1}>
+ @{typeaheadTooltipButtonText}
+ </Text>
+ </Button>
+ );
+}
+
+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<SuggestionItemType> = {
+text: string,
+matchedStrings: TypeaheadMatchedStrings,
- +suggestedUsers: $ReadOnlyArray<RelativeMemberInfo>,
+ +suggestions: $ReadOnlyArray<SuggestionItemType>,
+focusAndUpdateTextAndSelection: (text: string, selection: Selection) => void,
+ +typeaheadTooltipActionsGetter: (
+ TypeaheadTooltipActionsParams<SuggestionItemType>,
+ ) => $ReadOnlyArray<TypeaheadTooltipActionItem<SuggestionItemType>>,
+ +TypeaheadTooltipButtonComponent: TypeaheadTooltipButtonComponentType<SuggestionItemType>,
};
-function TypeaheadTooltip(props: TypeaheadTooltipProps): React.Node {
+function TypeaheadTooltip<SuggestionItemType>(
+ props: TypeaheadTooltipProps<SuggestionItemType>,
+): 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 (
- <Button onPress={onPress} style={styles.button} iosActiveOpacity={0.85}>
- <UserAvatar size="small" userID={item.id} />
- <Text style={styles.buttonLabel} numberOfLines={1}>
- @{item.username}
- </Text>
- </Button>
- );
- },
+ 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<SuggestionItemType>,
+ ...
+ }) => <TypeaheadTooltipButtonComponent item={item} />,
+ [],
+ );
+
// This is a hack that was introduced due to a buggy behavior of a
// absolutely positioned FlatList on Android.
@@ -95,17 +97,12 @@
<FlatList
style={styles.container}
contentContainerStyle={styles.contentContainer}
- data={suggestedUsers}
+ data={actions}
renderItem={renderTypeaheadButton}
keyboardShouldPersistTaps="always"
/>
),
- [
- 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<SuggestionItemType> = {
+ +suggestions: $ReadOnlyArray<SuggestionItemType>,
+ +textBeforeAtSymbol: string,
+ +text: string,
+ +query: string,
+ +focusAndUpdateTextAndSelection: (text: string, selection: Selection) => void,
+};
+function mentionTypeaheadTooltipActions({
+ suggestions,
+ textBeforeAtSymbol,
+ text,
+ query,
+ focusAndUpdateTextAndSelection,
+}: TypeaheadTooltipActionsParams<TypeaheadSuggestionItem>): $ReadOnlyArray<
+ TypeaheadTooltipActionItem<TypeaheadSuggestionItem>,
+> {
+ 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<SuggestionItemType> =
+ React.ComponentType<{
+ +item: TypeaheadTooltipActionItem<SuggestionItemType>,
+ }>;
+
+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);
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Dec 1, 12:51 AM (21 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2603032
Default Alt Text
D8897.id31500.diff (13 KB)
Attached To
Mode
D8897: [native] Refactor TypeaheadTooltip component
Attached
Detach File
Event Timeline
Log In to Comment