Add badges to avatars in group dialogs

This commit is contained in:
Evan Hahn 2021-11-20 09:41:21 -06:00 committed by GitHub
parent 7bb37dc63b
commit e490d91cc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 121 additions and 39 deletions

View File

@ -4,8 +4,9 @@
import type { ReactChild, ReactNode } from 'react';
import React from 'react';
import type { LocalizerType } from '../types/Util';
import type { LocalizerType, ThemeType } from '../types/Util';
import type { ConversationType } from '../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import { ModalHost } from './ModalHost';
import { Button, ButtonVariant } from './Button';
import { Avatar, AvatarSize } from './Avatar';
@ -92,20 +93,29 @@ GroupDialog.Paragraph = ({
type ContactsPropsType = {
contacts: Array<ConversationType>;
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
theme: ThemeType;
};
GroupDialog.Contacts = ({ contacts, i18n }: Readonly<ContactsPropsType>) => (
GroupDialog.Contacts = ({
contacts,
getPreferredBadge,
i18n,
theme,
}: Readonly<ContactsPropsType>) => (
<ul className="module-GroupDialog__contacts">
{contacts.map(contact => (
<li key={contact.id} className="module-GroupDialog__contacts__contact">
<Avatar
acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath}
badge={getPreferredBadge(contact.badges)}
color={contact.color}
conversationType={contact.type}
isMe={contact.isMe}
noteToSelf={contact.isMe}
theme={theme}
title={contact.title}
unblurredAvatarPath={contact.unblurredAvatarPath}
sharedGroupNames={contact.sharedGroupNames}

View File

@ -13,6 +13,7 @@ import type { ConversationType } from '../state/ducks/conversations';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { ThemeType } from '../types/Util';
const i18n = setupI18n('en', enMessages);
@ -44,6 +45,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
booleanOr(overrideProps.areWeInvited, false)
),
droppedMembers: overrideProps.droppedMembers || [contact3, contact1],
getPreferredBadge: () => undefined,
hasMigrated: boolean(
'hasMigrated',
booleanOr(overrideProps.hasMigrated, false)
@ -52,6 +54,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
invitedMembers: overrideProps.invitedMembers || [contact2],
migrate: action('migrate'),
onClose: action('onClose'),
theme: ThemeType.light,
});
const stories = storiesOf('Components/GroupV1MigrationDialog', module);

View File

@ -1,9 +1,10 @@
// Copyright 2019-2020 Signal Messenger, LLC
// Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import type { LocalizerType } from '../types/Util';
import type { LocalizerType, ThemeType } from '../types/Util';
import type { ConversationType } from '../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import { GroupDialog } from './GroupDialog';
import { sortByTitle } from '../util/sortByTitle';
@ -19,7 +20,9 @@ export type DataPropsType = {
};
export type HousekeepingPropsType = {
readonly getPreferredBadge: PreferredBadgeSelectorType;
readonly i18n: LocalizerType;
readonly theme: ThemeType;
};
export type PropsType = DataPropsType & HousekeepingPropsType;
@ -29,11 +32,13 @@ export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
const {
areWeInvited,
droppedMembers,
getPreferredBadge,
hasMigrated,
i18n,
invitedMembers,
migrate,
onClose,
theme,
} = props;
const title = hasMigrated
@ -84,23 +89,39 @@ export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
</GroupDialog.Paragraph>
) : (
<>
{renderMembers(
invitedMembers,
'GroupV1--Migration--info--invited',
i18n
)}
{renderMembers(droppedMembers, droppedMembersKey, i18n)}
{renderMembers({
getPreferredBadge,
i18n,
members: invitedMembers,
prefix: 'GroupV1--Migration--info--invited',
theme,
})}
{renderMembers({
getPreferredBadge,
i18n,
members: droppedMembers,
prefix: droppedMembersKey,
theme,
})}
</>
)}
</GroupDialog>
);
});
function renderMembers(
members: Array<ConversationType>,
prefix: string,
i18n: LocalizerType
): React.ReactNode {
function renderMembers({
getPreferredBadge,
i18n,
members,
prefix,
theme,
}: Readonly<{
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
members: Array<ConversationType>;
prefix: string;
theme: ThemeType;
}>): React.ReactNode {
if (!members.length) {
return null;
}
@ -111,7 +132,12 @@ function renderMembers(
return (
<>
<GroupDialog.Paragraph>{i18n(key)}</GroupDialog.Paragraph>
<GroupDialog.Contacts contacts={sortByTitle(members)} i18n={i18n} />
<GroupDialog.Contacts
contacts={sortByTitle(members)}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
theme={theme}
/>
</>
);
}

View File

@ -11,6 +11,7 @@ import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
import type { ConversationType } from '../state/ducks/conversations';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { ThemeType } from '../types/Util';
const i18n = setupI18n('en', enMessages);
@ -27,15 +28,19 @@ const story = storiesOf(
story.add('One contact', () => (
<NewlyCreatedGroupInvitedContactsDialog
contacts={[conversations[0]]}
getPreferredBadge={() => undefined}
i18n={i18n}
onClose={action('onClose')}
theme={ThemeType.light}
/>
));
story.add('Two contacts', () => (
<NewlyCreatedGroupInvitedContactsDialog
contacts={conversations}
getPreferredBadge={() => undefined}
i18n={i18n}
onClose={action('onClose')}
theme={ThemeType.light}
/>
));

View File

@ -4,8 +4,9 @@
import type { FunctionComponent, ReactNode } from 'react';
import React from 'react';
import type { LocalizerType } from '../types/Util';
import type { LocalizerType, ThemeType } from '../types/Util';
import type { ConversationType } from '../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import { Intl } from './Intl';
import { ContactName } from './conversation/ContactName';
import { GroupDialog } from './GroupDialog';
@ -13,12 +14,14 @@ import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
type PropsType = {
contacts: Array<ConversationType>;
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
onClose: () => void;
theme: ThemeType;
};
export const NewlyCreatedGroupInvitedContactsDialog: FunctionComponent<PropsType> =
({ contacts, i18n, onClose }) => {
({ contacts, getPreferredBadge, i18n, onClose, theme }) => {
let title: string;
let body: ReactNode;
if (contacts.length === 1) {
@ -57,7 +60,12 @@ export const NewlyCreatedGroupInvitedContactsDialog: FunctionComponent<PropsType
'NewlyCreatedGroupInvitedContactsDialog--body--info-paragraph'
)}
</GroupDialog.Paragraph>
<GroupDialog.Contacts contacts={contacts} i18n={i18n} />
<GroupDialog.Contacts
contacts={contacts}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
theme={theme}
/>
</>
);
}

View File

@ -12,6 +12,7 @@ import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
import type { PropsType } from './GroupV1Migration';
import { GroupV1Migration } from './GroupV1Migration';
import { ThemeType } from '../../types/Util';
const i18n = setupI18n('en', enMessages);
@ -33,8 +34,10 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
isBoolean(overrideProps.areWeInvited) ? overrideProps.areWeInvited : false
),
droppedMembers: overrideProps.droppedMembers || [contact1],
getPreferredBadge: () => undefined,
i18n,
invitedMembers: overrideProps.invitedMembers || [contact2],
theme: ThemeType.light,
});
const stories = storiesOf('Components/Conversation/GroupV1Migration', module);

View File

@ -5,8 +5,9 @@ import * as React from 'react';
import { Button, ButtonSize, ButtonVariant } from '../Button';
import { SystemMessage } from './SystemMessage';
import type { LocalizerType } from '../../types/Util';
import type { LocalizerType, ThemeType } from '../../types/Util';
import type { ConversationType } from '../../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
import { Intl } from '../Intl';
import { ContactName } from './ContactName';
import { GroupV1MigrationDialog } from '../GroupV1MigrationDialog';
@ -19,13 +20,22 @@ export type PropsDataType = {
};
export type PropsHousekeepingType = {
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
theme: ThemeType;
};
export type PropsType = PropsDataType & PropsHousekeepingType;
export function GroupV1Migration(props: PropsType): React.ReactElement {
const { areWeInvited, droppedMembers, i18n, invitedMembers } = props;
const {
areWeInvited,
droppedMembers,
getPreferredBadge,
i18n,
invitedMembers,
theme,
} = props;
const [showingDialog, setShowingDialog] = React.useState(false);
const showDialog = React.useCallback(() => {
@ -77,11 +87,13 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
<GroupV1MigrationDialog
areWeInvited={areWeInvited}
droppedMembers={droppedMembers}
getPreferredBadge={getPreferredBadge}
hasMigrated
i18n={i18n}
invitedMembers={invitedMembers}
migrate={() => log.warn('GroupV1Migration: Modal called migrate()')}
onClose={dismissDialog}
theme={theme}
/>
) : null}
</>

View File

@ -457,8 +457,10 @@ const renderTypingBubble = () => (
/>
);
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
getPreferredBadge: () => undefined,
i18n,
theme: React.useContext(StorybookThemeContext),
haveNewest: boolean('haveNewest', overrideProps.haveNewest !== false),
haveOldest: boolean('haveOldest', overrideProps.haveOldest !== false),
@ -494,13 +496,13 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
});
story.add('Oldest and Newest', () => {
const props = createProps();
const props = useProps();
return <Timeline {...props} />;
});
story.add('With active message request', () => {
const props = createProps({
const props = useProps({
isIncomingMessageRequest: true,
});
@ -508,7 +510,7 @@ story.add('With active message request', () => {
});
story.add('Without Newest Message', () => {
const props = createProps({
const props = useProps({
haveNewest: false,
});
@ -516,7 +518,7 @@ story.add('Without Newest Message', () => {
});
story.add('Without newest message, active message request', () => {
const props = createProps({
const props = useProps({
haveOldest: false,
isIncomingMessageRequest: true,
});
@ -525,7 +527,7 @@ story.add('Without newest message, active message request', () => {
});
story.add('Without Oldest Message', () => {
const props = createProps({
const props = useProps({
haveOldest: false,
scrollToIndex: -1,
});
@ -534,7 +536,7 @@ story.add('Without Oldest Message', () => {
});
story.add('Empty (just hero)', () => {
const props = createProps({
const props = useProps({
items: [],
});
@ -542,7 +544,7 @@ story.add('Empty (just hero)', () => {
});
story.add('Last Seen', () => {
const props = createProps({
const props = useProps({
oldestUnreadIndex: 13,
totalUnread: 2,
});
@ -551,7 +553,7 @@ story.add('Last Seen', () => {
});
story.add('Target Index to Top', () => {
const props = createProps({
const props = useProps({
scrollToIndex: 0,
});
@ -559,7 +561,7 @@ story.add('Target Index to Top', () => {
});
story.add('Typing Indicator', () => {
const props = createProps({
const props = useProps({
typingContactId: UUID.generate().toString(),
});
@ -567,7 +569,7 @@ story.add('Typing Indicator', () => {
});
story.add('With invited contacts for a newly-created group', () => {
const props = createProps({
const props = useProps({
invitedContactsForNewlyCreatedGroup: [
getDefaultConversation({
id: 'abc123',
@ -584,7 +586,7 @@ story.add('With invited contacts for a newly-created group', () => {
});
story.add('With "same name in direct conversation" warning', () => {
const props = createProps({
const props = useProps({
warning: {
type: ContactSpoofingType.DirectConversationWithSameTitle,
safeConversation: getDefaultConversation(),
@ -596,7 +598,7 @@ story.add('With "same name in direct conversation" warning', () => {
});
story.add('With "same name in group conversation" warning', () => {
const props = createProps({
const props = useProps({
warning: {
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
acknowledgedGroupNameCollisions: {},

View File

@ -17,8 +17,9 @@ import Measure from 'react-measure';
import { ScrollDownButton } from './ScrollDownButton';
import type { AssertProps, LocalizerType } from '../../types/Util';
import type { AssertProps, LocalizerType, ThemeType } from '../../types/Util';
import type { ConversationType } from '../../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
import { assert } from '../../util/assert';
import { missingCaseError } from '../../util/missingCaseError';
import { createRefMerger } from '../../util/refMerger';
@ -102,7 +103,9 @@ type PropsHousekeepingType = {
warning?: WarningType;
contactSpoofingReview?: ContactSpoofingReviewPropType;
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
theme: ThemeType;
renderItem: (props: {
actionProps: PropsActionsType;
@ -1312,6 +1315,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
clearInvitedUuidsForNewlyCreatedGroup,
closeContactSpoofingReview,
contactSpoofingReview,
getPreferredBadge,
i18n,
id,
invitedContactsForNewlyCreatedGroup,
@ -1325,6 +1329,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
removeMember,
reviewGroupMemberNameCollision,
reviewMessageRequestNameCollision,
theme,
} = this.props;
const {
shouldShowScrollDownButton,
@ -1561,8 +1566,10 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
{Boolean(invitedContactsForNewlyCreatedGroup.length) && (
<NewlyCreatedGroupInvitedContactsDialog
contacts={invitedContactsForNewlyCreatedGroup}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
onClose={clearInvitedUuidsForNewlyCreatedGroup}
theme={theme}
/>
)}

View File

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
@ -7,9 +7,10 @@ import type { PropsType as GroupV1MigrationDialogPropsType } from '../../compone
import { GroupV1MigrationDialog } from '../../components/GroupV1MigrationDialog';
import type { ConversationType } from '../ducks/conversations';
import type { StateType } from '../reducer';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
import { getIntl, getTheme } from '../selectors/user';
import * as log from '../../logging/log';
export type PropsType = {
@ -17,7 +18,7 @@ export type PropsType = {
readonly invitedMemberIds: Array<string>;
} & Omit<
GroupV1MigrationDialogPropsType,
'i18n' | 'droppedMembers' | 'invitedMembers'
'i18n' | 'droppedMembers' | 'invitedMembers' | 'theme' | 'getPreferredBadge'
>;
const mapStateToProps = (
@ -44,8 +45,10 @@ const mapStateToProps = (
return {
...props,
droppedMembers,
getPreferredBadge: getPreferredBadgeSelector(state),
invitedMembers,
i18n: getIntl(state),
theme: getTheme(state),
};
};

View File

@ -18,7 +18,7 @@ import { Timeline } from '../../components/conversation/Timeline';
import type { StateType } from '../reducer';
import type { ConversationType } from '../ducks/conversations';
import { getIntl } from '../selectors/user';
import { getIntl, getTheme } from '../selectors/user';
import {
getConversationByUuidSelector,
getConversationMessagesSelector,
@ -48,6 +48,7 @@ import {
} from '../../util/groupMemberNameCollisions';
import { ContactSpoofingType } from '../../util/contactSpoofing';
import type { WidthBreakpoint } from '../../components/_util';
import { getPreferredBadgeSelector } from '../selectors/badges';
type ExternalProps = {
id: string;
@ -313,7 +314,9 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
warning: getWarning(conversation, state),
contactSpoofingReview: getContactSpoofingReview(id, state),
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
theme: getTheme(state),
renderItem,
renderLastSeenIndicator,
renderHeroRow,