Add badges to @-mentions picker

Co-authored-by: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com>
This commit is contained in:
automated-signal 2021-11-17 10:53:20 -08:00 committed by GitHub
parent 21915e93f7
commit 791176625b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 55 additions and 21 deletions

View File

@ -12,10 +12,10 @@ import type { Props } from './CompositionArea';
import { CompositionArea } from './CompositionArea'; import { CompositionArea } from './CompositionArea';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
import { fakeDraftAttachment } from '../test-both/helpers/fakeAttachment'; import { fakeDraftAttachment } from '../test-both/helpers/fakeAttachment';
import { landscapeGreenUrl } from '../storybook/Fixtures'; import { landscapeGreenUrl } from '../storybook/Fixtures';
import { ThemeType } from '../types/Util';
import { RecordingState } from '../state/ducks/audioRecorder'; import { RecordingState } from '../state/ducks/audioRecorder';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -25,7 +25,7 @@ const story = storiesOf('Components/CompositionArea', module);
// necessary for the add attachment button to render properly // necessary for the add attachment button to render properly
story.addDecorator(storyFn => <div className="file-input">{storyFn()}</div>); story.addDecorator(storyFn => <div className="file-input">{storyFn()}</div>);
const createProps = (overrideProps: Partial<Props> = {}): Props => ({ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
addAttachment: action('addAttachment'), addAttachment: action('addAttachment'),
addPendingAttachment: action('addPendingAttachment'), addPendingAttachment: action('addPendingAttachment'),
conversationId: '123', conversationId: '123',
@ -33,7 +33,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
onSendMessage: action('onSendMessage'), onSendMessage: action('onSendMessage'),
processAttachments: action('processAttachments'), processAttachments: action('processAttachments'),
removeAttachment: action('removeAttachment'), removeAttachment: action('removeAttachment'),
theme: ThemeType.light, theme: React.useContext(StorybookThemeContext),
// AttachmentList // AttachmentList
draftAttachments: overrideProps.draftAttachments || [], draftAttachments: overrideProps.draftAttachments || [],
@ -66,6 +66,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
onTextTooLong: action('onTextTooLong'), onTextTooLong: action('onTextTooLong'),
draftText: overrideProps.draftText || undefined, draftText: overrideProps.draftText || undefined,
clearQuotedMessage: action('clearQuotedMessage'), clearQuotedMessage: action('clearQuotedMessage'),
getPreferredBadge: () => undefined,
getQuotedMessage: action('getQuotedMessage'), getQuotedMessage: action('getQuotedMessage'),
scrollToBottom: action('scrollToBottom'), scrollToBottom: action('scrollToBottom'),
sortedGroupMembers: [], sortedGroupMembers: [],
@ -115,13 +116,13 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
}); });
story.add('Default', () => { story.add('Default', () => {
const props = createProps(); const props = useProps();
return <CompositionArea {...props} />; return <CompositionArea {...props} />;
}); });
story.add('Starting Text', () => { story.add('Starting Text', () => {
const props = createProps({ const props = useProps({
draftText: "here's some starting text", draftText: "here's some starting text",
}); });
@ -129,7 +130,7 @@ story.add('Starting Text', () => {
}); });
story.add('Sticker Button', () => { story.add('Sticker Button', () => {
const props = createProps({ const props = useProps({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
knownPacks: [{} as any], knownPacks: [{} as any],
}); });
@ -138,7 +139,7 @@ story.add('Sticker Button', () => {
}); });
story.add('Message Request', () => { story.add('Message Request', () => {
const props = createProps({ const props = useProps({
messageRequestsEnabled: true, messageRequestsEnabled: true,
}); });
@ -146,7 +147,7 @@ story.add('Message Request', () => {
}); });
story.add('SMS-only fetching UUID', () => { story.add('SMS-only fetching UUID', () => {
const props = createProps({ const props = useProps({
isSMSOnly: true, isSMSOnly: true,
isFetchingUUID: true, isFetchingUUID: true,
}); });
@ -155,7 +156,7 @@ story.add('SMS-only fetching UUID', () => {
}); });
story.add('SMS-only', () => { story.add('SMS-only', () => {
const props = createProps({ const props = useProps({
isSMSOnly: true, isSMSOnly: true,
}); });
@ -163,7 +164,7 @@ story.add('SMS-only', () => {
}); });
story.add('Attachments', () => { story.add('Attachments', () => {
const props = createProps({ const props = useProps({
draftAttachments: [ draftAttachments: [
fakeDraftAttachment({ fakeDraftAttachment({
contentType: IMAGE_JPEG, contentType: IMAGE_JPEG,
@ -177,7 +178,7 @@ story.add('Attachments', () => {
story.add('Announcements Only group', () => ( story.add('Announcements Only group', () => (
<CompositionArea <CompositionArea
{...createProps({ {...useProps({
announcementsOnly: true, announcementsOnly: true,
areWeAdmin: false, areWeAdmin: false,
})} })}

View File

@ -135,6 +135,7 @@ export type Props = Pick<
| 'draftText' | 'draftText'
| 'draftBodyRanges' | 'draftBodyRanges'
| 'clearQuotedMessage' | 'clearQuotedMessage'
| 'getPreferredBadge'
| 'getQuotedMessage' | 'getQuotedMessage'
> & > &
Pick< Pick<
@ -200,6 +201,7 @@ export const CompositionArea = ({
draftText, draftText,
draftBodyRanges, draftBodyRanges,
clearQuotedMessage, clearQuotedMessage,
getPreferredBadge,
getQuotedMessage, getQuotedMessage,
scrollToBottom, scrollToBottom,
sortedGroupMembers, sortedGroupMembers,
@ -634,6 +636,7 @@ export const CompositionArea = ({
disabled={disabled} disabled={disabled}
draftBodyRanges={draftBodyRanges} draftBodyRanges={draftBodyRanges}
draftText={draftText} draftText={draftText}
getPreferredBadge={getPreferredBadge}
getQuotedMessage={getQuotedMessage} getQuotedMessage={getQuotedMessage}
inputApi={inputApiRef} inputApi={inputApiRef}
large={large} large={large}
@ -645,6 +648,7 @@ export const CompositionArea = ({
scrollToBottom={scrollToBottom} scrollToBottom={scrollToBottom}
skinTone={skinTone} skinTone={skinTone}
sortedGroupMembers={sortedGroupMembers} sortedGroupMembers={sortedGroupMembers}
theme={theme}
/> />
</div> </div>
{!large ? ( {!large ? (

View File

@ -13,12 +13,13 @@ import type { Props } from './CompositionInput';
import { CompositionInput } from './CompositionInput'; import { CompositionInput } from './CompositionInput';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/CompositionInput', module); const story = storiesOf('Components/CompositionInput', module);
const createProps = (overrideProps: Partial<Props> = {}): Props => ({ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
i18n, i18n,
conversationId: 'conversation-id', conversationId: 'conversation-id',
disabled: boolean('disabled', overrideProps.disabled || false), disabled: boolean('disabled', overrideProps.disabled || false),
@ -28,6 +29,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
draftText: overrideProps.draftText || undefined, draftText: overrideProps.draftText || undefined,
draftBodyRanges: overrideProps.draftBodyRanges || [], draftBodyRanges: overrideProps.draftBodyRanges || [],
clearQuotedMessage: action('clearQuotedMessage'), clearQuotedMessage: action('clearQuotedMessage'),
getPreferredBadge: () => undefined,
getQuotedMessage: action('getQuotedMessage'), getQuotedMessage: action('getQuotedMessage'),
onPickEmoji: action('onPickEmoji'), onPickEmoji: action('onPickEmoji'),
large: boolean('large', overrideProps.large || false), large: boolean('large', overrideProps.large || false),
@ -45,16 +47,17 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
}, },
overrideProps.skinTone || undefined overrideProps.skinTone || undefined
), ),
theme: React.useContext(StorybookThemeContext),
}); });
story.add('Default', () => { story.add('Default', () => {
const props = createProps(); const props = useProps();
return <CompositionInput {...props} />; return <CompositionInput {...props} />;
}); });
story.add('Large', () => { story.add('Large', () => {
const props = createProps({ const props = useProps({
large: true, large: true,
}); });
@ -62,7 +65,7 @@ story.add('Large', () => {
}); });
story.add('Disabled', () => { story.add('Disabled', () => {
const props = createProps({ const props = useProps({
disabled: true, disabled: true,
}); });
@ -70,7 +73,7 @@ story.add('Disabled', () => {
}); });
story.add('Starting Text', () => { story.add('Starting Text', () => {
const props = createProps({ const props = useProps({
draftText: "here's some starting text", draftText: "here's some starting text",
}); });
@ -78,7 +81,7 @@ story.add('Starting Text', () => {
}); });
story.add('Multiline Text', () => { story.add('Multiline Text', () => {
const props = createProps({ const props = useProps({
draftText: `here's some starting text draftText: `here's some starting text
and more on another line and more on another line
and yet another line and yet another line
@ -94,7 +97,7 @@ and we're done`,
}); });
story.add('Emojis', () => { story.add('Emojis', () => {
const props = createProps({ const props = useProps({
draftText: `⁣😐😐😐😐😐😐😐 draftText: `⁣😐😐😐😐😐😐😐
😐😐😐😐😐😐😐 😐😐😐😐😐😐😐
😐😐😐😂😐😐😐 😐😐😐😂😐😐😐
@ -106,7 +109,7 @@ story.add('Emojis', () => {
}); });
story.add('Mentions', () => { story.add('Mentions', () => {
const props = createProps({ const props = useProps({
sortedGroupMembers: [ sortedGroupMembers: [
getDefaultConversation({ getDefaultConversation({
title: 'Kate Beaton', title: 'Kate Beaton',

View File

@ -14,8 +14,9 @@ import { MentionCompletion } from '../quill/mentions/completion';
import { EmojiBlot, EmojiCompletion } from '../quill/emoji'; import { EmojiBlot, EmojiCompletion } from '../quill/emoji';
import type { EmojiPickDataType } from './emoji/EmojiPicker'; import type { EmojiPickDataType } from './emoji/EmojiPicker';
import { convertShortName } from './emoji/lib'; import { convertShortName } from './emoji/lib';
import type { LocalizerType, BodyRangeType } from '../types/Util'; import type { LocalizerType, BodyRangeType, ThemeType } from '../types/Util';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import { isValidUuid } from '../types/UUID'; import { isValidUuid } from '../types/UUID';
import { MentionBlot } from '../quill/mentions/blot'; import { MentionBlot } from '../quill/mentions/blot';
import { import {
@ -63,12 +64,14 @@ export type Props = {
readonly i18n: LocalizerType; readonly i18n: LocalizerType;
readonly conversationId: string; readonly conversationId: string;
readonly disabled?: boolean; readonly disabled?: boolean;
readonly getPreferredBadge: PreferredBadgeSelectorType;
readonly large?: boolean; readonly large?: boolean;
readonly inputApi?: React.MutableRefObject<InputApi | undefined>; readonly inputApi?: React.MutableRefObject<InputApi | undefined>;
readonly skinTone?: EmojiPickDataType['skinTone']; readonly skinTone?: EmojiPickDataType['skinTone'];
readonly draftText?: string; readonly draftText?: string;
readonly draftBodyRanges?: Array<BodyRangeType>; readonly draftBodyRanges?: Array<BodyRangeType>;
readonly moduleClassName?: string; readonly moduleClassName?: string;
readonly theme: ThemeType;
sortedGroupMembers?: Array<ConversationType>; sortedGroupMembers?: Array<ConversationType>;
onDirtyChange?(dirty: boolean): unknown; onDirtyChange?(dirty: boolean): unknown;
onEditorStateChange?( onEditorStateChange?(
@ -104,10 +107,12 @@ export function CompositionInput(props: Props): React.ReactElement {
skinTone, skinTone,
draftText, draftText,
draftBodyRanges, draftBodyRanges,
getPreferredBadge,
getQuotedMessage, getQuotedMessage,
clearQuotedMessage, clearQuotedMessage,
scrollToBottom, scrollToBottom,
sortedGroupMembers, sortedGroupMembers,
theme,
} = props; } = props;
const [emojiCompletionElement, setEmojiCompletionElement] = const [emojiCompletionElement, setEmojiCompletionElement] =
@ -550,12 +555,14 @@ export function CompositionInput(props: Props): React.ReactElement {
skinTone, skinTone,
}, },
mentionCompletion: { mentionCompletion: {
getPreferredBadge,
me: sortedGroupMembers me: sortedGroupMembers
? sortedGroupMembers.find(foo => foo.isMe) ? sortedGroupMembers.find(foo => foo.isMe)
: undefined, : undefined,
memberRepositoryRef, memberRepositoryRef,
setMentionPickerElement: setMentionCompletionElement, setMentionPickerElement: setMentionCompletionElement,
i18n, i18n,
theme,
}, },
}} }}
formats={['emoji', 'mention']} formats={['emoji', 'mention']}

View File

@ -47,6 +47,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
conversationId: 'conversation-id', conversationId: 'conversation-id',
candidateConversations, candidateConversations,
doForwardMessage: action('doForwardMessage'), doForwardMessage: action('doForwardMessage'),
getPreferredBadge: () => undefined,
i18n, i18n,
isSticker: Boolean(overrideProps.isSticker), isSticker: Boolean(overrideProps.isSticker),
linkPreview: overrideProps.linkPreview, linkPreview: overrideProps.linkPreview,

View File

@ -25,6 +25,7 @@ import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbo
import type { Row } from './ConversationList'; import type { Row } from './ConversationList';
import { ConversationList, RowType } from './ConversationList'; import { ConversationList, RowType } from './ConversationList';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { Props as EmojiButtonProps } from './emoji/EmojiButton'; import type { Props as EmojiButtonProps } from './emoji/EmojiButton';
import { EmojiButton } from './emoji/EmojiButton'; import { EmojiButton } from './emoji/EmojiButton';
import type { EmojiPickDataType } from './emoji/EmojiPicker'; import type { EmojiPickDataType } from './emoji/EmojiPicker';
@ -47,6 +48,7 @@ export type DataPropsType = {
attachments?: Array<AttachmentDraftType>, attachments?: Array<AttachmentDraftType>,
linkPreview?: LinkPreviewType linkPreview?: LinkPreviewType
) => void; ) => void;
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType; i18n: LocalizerType;
isSticker: boolean; isSticker: boolean;
linkPreview?: LinkPreviewType; linkPreview?: LinkPreviewType;
@ -77,6 +79,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
candidateConversations, candidateConversations,
conversationId, conversationId,
doForwardMessage, doForwardMessage,
getPreferredBadge,
i18n, i18n,
isSticker, isSticker,
linkPreview, linkPreview,
@ -337,6 +340,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
conversationId={conversationId} conversationId={conversationId}
clearQuotedMessage={shouldNeverBeCalled} clearQuotedMessage={shouldNeverBeCalled}
draftText={messageBodyText} draftText={messageBodyText}
getPreferredBadge={getPreferredBadge}
getQuotedMessage={noop} getQuotedMessage={noop}
i18n={i18n} i18n={i18n}
inputApi={inputApiRef} inputApi={inputApiRef}
@ -354,6 +358,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
onPickEmoji={onPickEmoji} onPickEmoji={onPickEmoji}
onSubmit={forwardMessage} onSubmit={forwardMessage}
onTextTooLong={onTextTooLong} onTextTooLong={onTextTooLong}
theme={theme}
/> />
<div className="module-ForwardMessageModal__emoji"> <div className="module-ForwardMessageModal__emoji">
<EmojiButton <EmojiButton

View File

@ -12,16 +12,19 @@ import classNames from 'classnames';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import type { ConversationType } from '../../state/ducks/conversations'; import type { ConversationType } from '../../state/ducks/conversations';
import { Avatar } from '../../components/Avatar'; import { Avatar } from '../../components/Avatar';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType, ThemeType } from '../../types/Util';
import type { MemberRepository } from '../memberRepository'; import type { MemberRepository } from '../memberRepository';
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
import { matchBlotTextPartitions } from '../util'; import { matchBlotTextPartitions } from '../util';
import { sameWidthModifier } from '../../util/popperUtil'; import { sameWidthModifier } from '../../util/popperUtil';
export type MentionCompletionOptions = { export type MentionCompletionOptions = {
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType; i18n: LocalizerType;
memberRepositoryRef: RefObject<MemberRepository>; memberRepositoryRef: RefObject<MemberRepository>;
setMentionPickerElement: (element: JSX.Element | null) => void; setMentionPickerElement: (element: JSX.Element | null) => void;
me?: ConversationType; me?: ConversationType;
theme: ThemeType;
}; };
declare global { declare global {
@ -213,6 +216,7 @@ export class MentionCompletion {
render(): void { render(): void {
const { results: memberResults, index: memberResultsIndex } = this; const { results: memberResults, index: memberResultsIndex } = this;
const { getPreferredBadge, theme } = this.options;
if (memberResults.length === 0) { if (memberResults.length === 0) {
this.options.setMentionPickerElement(null); this.options.setMentionPickerElement(null);
@ -258,11 +262,13 @@ export class MentionCompletion {
<Avatar <Avatar
acceptedMessageRequest={member.acceptedMessageRequest} acceptedMessageRequest={member.acceptedMessageRequest}
avatarPath={member.avatarPath} avatarPath={member.avatarPath}
badge={getPreferredBadge(member.badges)}
conversationType="direct" conversationType="direct"
i18n={this.options.i18n} i18n={this.options.i18n}
isMe={member.isMe} isMe={member.isMe}
sharedGroupNames={member.sharedGroupNames} sharedGroupNames={member.sharedGroupNames}
size={28} size={28}
theme={theme}
title={member.title} title={member.title}
unblurredAvatarPath={member.unblurredAvatarPath} unblurredAvatarPath={member.unblurredAvatarPath}
/> />

View File

@ -10,6 +10,7 @@ import type { StateType } from '../reducer';
import { isConversationSMSOnly } from '../../util/isConversationSMSOnly'; import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
import { dropNull } from '../../util/dropNull'; import { dropNull } from '../../util/dropNull';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { selectRecentEmojis } from '../selectors/emojis'; import { selectRecentEmojis } from '../selectors/emojis';
import { getIntl, getTheme, getUserConversationId } from '../selectors/user'; import { getIntl, getTheme, getUserConversationId } from '../selectors/user';
import { getEmojiSkinTone } from '../selectors/items'; import { getEmojiSkinTone } from '../selectors/items';
@ -80,6 +81,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
conversationId: id, conversationId: id,
i18n: getIntl(state), i18n: getIntl(state),
theme: getTheme(state), theme: getTheme(state),
getPreferredBadge: getPreferredBadgeSelector(state),
// AudioCapture // AudioCapture
errorDialogAudioRecorderType: errorDialogAudioRecorderType:
state.audioRecorder.errorDialogAudioRecorderType, state.audioRecorder.errorDialogAudioRecorderType,

View File

@ -8,6 +8,7 @@ import { ForwardMessageModal } from '../../components/ForwardMessageModal';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import type { BodyRangeType } from '../../types/Util'; import type { BodyRangeType } from '../../types/Util';
import type { LinkPreviewType } from '../../types/message/LinkPreviews'; import type { LinkPreviewType } from '../../types/message/LinkPreviews';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getAllComposableConversations } from '../selectors/conversations'; import { getAllComposableConversations } from '../selectors/conversations';
import { getLinkPreview } from '../selectors/linkPreviews'; import { getLinkPreview } from '../selectors/linkPreviews';
import { getIntl, getTheme } from '../selectors/user'; import { getIntl, getTheme } from '../selectors/user';
@ -60,6 +61,7 @@ const mapStateToProps = (
candidateConversations, candidateConversations,
conversationId, conversationId,
doForwardMessage, doForwardMessage,
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state), i18n: getIntl(state),
isSticker, isSticker,
linkPreview, linkPreview,

View File

@ -12,6 +12,7 @@ import type { MentionCompletionOptions } from '../../../quill/mentions/completio
import { MentionCompletion } from '../../../quill/mentions/completion'; import { MentionCompletion } from '../../../quill/mentions/completion';
import type { ConversationType } from '../../../state/ducks/conversations'; import type { ConversationType } from '../../../state/ducks/conversations';
import { MemberRepository } from '../../../quill/memberRepository'; import { MemberRepository } from '../../../quill/memberRepository';
import { ThemeType } from '../../../types/Util';
import { getDefaultConversationWithUuid } from '../../../test-both/helpers/getDefaultConversation'; import { getDefaultConversationWithUuid } from '../../../test-both/helpers/getDefaultConversation';
const me: ConversationType = getDefaultConversationWithUuid({ const me: ConversationType = getDefaultConversationWithUuid({
@ -65,10 +66,12 @@ describe('MentionCompletion', () => {
}; };
const options: MentionCompletionOptions = { const options: MentionCompletionOptions = {
getPreferredBadge: () => undefined,
i18n: Object.assign(sinon.stub(), { getLocale: sinon.stub() }), i18n: Object.assign(sinon.stub(), { getLocale: sinon.stub() }),
me, me,
memberRepositoryRef, memberRepositoryRef,
setMentionPickerElement: sinon.stub(), setMentionPickerElement: sinon.stub(),
theme: ThemeType.dark,
}; };
mockQuill = { mockQuill = {