Changeset View
Changeset View
Standalone View
Standalone View
web/chat/mention-utils.js
// @flow | // @flow | ||||
import * as React from 'react'; | |||||
import { oldValidUsernameRegexString } from 'lib/shared/account-utils'; | import { oldValidUsernameRegexString } from 'lib/shared/account-utils'; | ||||
import SearchIndex from 'lib/shared/search-index'; | import SearchIndex from 'lib/shared/search-index'; | ||||
import { stringForUserExplicit } from 'lib/shared/user-utils'; | import { stringForUserExplicit } from 'lib/shared/user-utils'; | ||||
import type { RelativeMemberInfo } from 'lib/types/thread-types'; | import type { RelativeMemberInfo } from 'lib/types/thread-types'; | ||||
const mentionRegex: RegExp = new RegExp( | const mentionRegex: RegExp = new RegExp( | ||||
`(?<textPrefix>(?:^(?:.|\n)*\\s+)|^)@(?<username>${oldValidUsernameRegexString})?$`, | `(?<textPrefix>(?:^(?:.|\n)*\\s+)|^)@(?<username>${oldValidUsernameRegexString})?$`, | ||||
); | ); | ||||
import { type InputState } from '../input/input-state'; | |||||
import { typeaheadStyle } from './chat-constants'; | |||||
export type MentionSuggestionTooltipAction = { | |||||
+key: string, | |||||
+onClick: (SyntheticEvent<HTMLButtonElement>) => mixed, | |||||
+actionButtonContent: React.Node, | |||||
}; | |||||
export type TooltipPosition = { | |||||
+top: number, | |||||
+left: number, | |||||
}; | |||||
function getTypeaheadUserSuggestions( | function getTypeaheadUserSuggestions( | ||||
userSearchIndex: SearchIndex, | userSearchIndex: SearchIndex, | ||||
usersInThread: $ReadOnlyArray<RelativeMemberInfo>, | usersInThread: $ReadOnlyArray<RelativeMemberInfo>, | ||||
typedPrefix: string, | typedPrefix: string, | ||||
): $ReadOnlyArray<RelativeMemberInfo> { | ): $ReadOnlyArray<RelativeMemberInfo> { | ||||
const userIDs = userSearchIndex.getSearchResults(typedPrefix); | const userIDs = userSearchIndex.getSearchResults(typedPrefix); | ||||
return usersInThread | return usersInThread | ||||
.filter(user => typedPrefix.length === 0 || userIDs.includes(user.id)) | .filter(user => typedPrefix.length === 0 || userIDs.includes(user.id)) | ||||
.sort((userA, userB) => | .sort((userA, userB) => | ||||
stringForUserExplicit(userA).localeCompare(stringForUserExplicit(userB)), | stringForUserExplicit(userA).localeCompare(stringForUserExplicit(userB)), | ||||
); | ); | ||||
} | } | ||||
function getCaretOffsets( | function getCaretOffsets( | ||||
textarea: ?HTMLTextAreaElement, | textarea: HTMLTextAreaElement, | ||||
text: string, | text: string, | ||||
): { caretTopOffset: number, caretLeftOffset: number } { | ): { caretTopOffset: number, caretLeftOffset: number } { | ||||
if (!textarea) { | if (!textarea) { | ||||
return { caretTopOffset: 0, caretLeftOffset: 0 }; | return { caretTopOffset: 0, caretLeftOffset: 0 }; | ||||
} | } | ||||
// terribly hacky but it works I guess :D | // terribly hacky but it works I guess :D | ||||
// we had to use it, as it's hard to count lines in textarea | // we had to use it, as it's hard to count lines in textarea | ||||
Show All 17 Lines | ): { caretTopOffset: number, caretLeftOffset: number } { | ||||
const span = document.createElement('span'); | const span = document.createElement('span'); | ||||
span.textContent = textarea.value.slice(text.length); | span.textContent = textarea.value.slice(text.length); | ||||
div.appendChild(span); | div.appendChild(span); | ||||
document.body?.appendChild(div); | document.body?.appendChild(div); | ||||
const { offsetTop, offsetLeft } = span; | const { offsetTop, offsetLeft } = span; | ||||
document.body?.removeChild(div); | document.body?.removeChild(div); | ||||
const textareaWidth = parseInt(textareaStyle.getPropertyValue('width')); | |||||
const caretLeftOffset = | |||||
offsetLeft + typeaheadStyle.tooltipWidth > textareaWidth | |||||
? textareaWidth - typeaheadStyle.tooltipWidth | |||||
: offsetLeft; | |||||
return { | return { | ||||
caretTopOffset: offsetTop - textarea.scrollTop, | caretTopOffset: offsetTop - textarea.scrollTop, | ||||
caretLeftOffset: offsetLeft, | caretLeftOffset, | ||||
}; | }; | ||||
} | } | ||||
export { mentionRegex, getTypeaheadUserSuggestions, getCaretOffsets }; | function getTypeaheadTooltipActions( | ||||
inputState: InputState, | |||||
textarea: HTMLTextAreaElement, | |||||
suggestedUsers: $ReadOnlyArray<RelativeMemberInfo>, | |||||
matchedTextBefore: string, | |||||
wholeMatch: string, | |||||
): $ReadOnlyArray<MentionSuggestionTooltipAction> { | |||||
return suggestedUsers | |||||
.filter( | |||||
suggestedUser => stringForUserExplicit(suggestedUser) !== 'anonymous', | |||||
) | |||||
.map(suggestedUser => ({ | |||||
key: suggestedUser.id, | |||||
onClick: () => { | |||||
const newPrefixText = matchedTextBefore; | |||||
let newSuffixText = inputState.draft.slice(wholeMatch.length); | |||||
newSuffixText = (newSuffixText[0] !== ' ' ? ' ' : '') + newSuffixText; | |||||
const newText = | |||||
newPrefixText + | |||||
'@' + | |||||
stringForUserExplicit(suggestedUser) + | |||||
newSuffixText; | |||||
inputState.setDraft(newText); | |||||
inputState.setTextCursorPosition( | |||||
newText.length - newSuffixText.length + 1, | |||||
); | |||||
}, | |||||
actionButtonContent: stringForUserExplicit(suggestedUser), | |||||
})); | |||||
} | |||||
function getTypeaheadTooltipPosition( | |||||
textarea: HTMLTextAreaElement, | |||||
actionsLength: number, | |||||
matchedTextBefore: string, | |||||
): TooltipPosition { | |||||
const { caretTopOffset, caretLeftOffset } = getCaretOffsets( | |||||
textarea, | |||||
matchedTextBefore, | |||||
); | |||||
const textareaBoundingClientRect = textarea.getBoundingClientRect(); | |||||
const top: number = | |||||
textareaBoundingClientRect.top - | |||||
Math.min( | |||||
typeaheadStyle.tooltipVerticalPadding + | |||||
actionsLength * typeaheadStyle.rowHeight, | |||||
typeaheadStyle.tooltipMaxHeight, | |||||
) - | |||||
typeaheadStyle.tooltipTopOffset + | |||||
caretTopOffset; | |||||
const left: number = | |||||
textareaBoundingClientRect.left - | |||||
typeaheadStyle.tooltipLeftOffset + | |||||
caretLeftOffset; | |||||
return { top, left }; | |||||
} | |||||
export { | |||||
mentionRegex, | |||||
getTypeaheadUserSuggestions, | |||||
getCaretOffsets, | |||||
getTypeaheadTooltipActions, | |||||
getTypeaheadTooltipPosition, | |||||
}; |