Add story entry points around the app

This commit is contained in:
Josh Perez 2022-07-21 20:44:35 -04:00 committed by GitHub
parent 1d5b361159
commit 5dfe30d235
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 367 additions and 533 deletions

View File

@ -119,6 +119,7 @@
margin-left: 4px;
margin-right: var(--button-spacing);
padding: $padding;
padding-left: 0;
@include keyboard-mode {
&:focus {

View File

@ -3,20 +3,20 @@
import type { Meta, Story } from '@storybook/react';
import * as React from 'react';
import { isBoolean } from 'lodash';
import { action } from '@storybook/addon-actions';
import { expect } from '@storybook/jest';
import { isBoolean } from 'lodash';
import { within, userEvent } from '@storybook/testing-library';
import { action } from '@storybook/addon-actions';
import type { Props } from './Avatar';
import { Avatar, AvatarBlur, AvatarSize, AvatarStoryRing } from './Avatar';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
import type { AvatarColorType } from '../types/Colors';
import type { Props } from './Avatar';
import enMessages from '../../_locales/en/messages.json';
import { Avatar, AvatarBlur, AvatarSize } from './Avatar';
import { AvatarColors } from '../types/Colors';
import { getFakeBadge } from '../test-both/helpers/getFakeBadge';
import { HasStories } from '../types/Stories';
import { ThemeType } from '../types/Util';
import { getFakeBadge } from '../test-both/helpers/getFakeBadge';
import { setupI18n } from '../util/setupI18n';
const i18n = setupI18n('en', enMessages);
@ -63,7 +63,7 @@ export default {
},
storyRing: {
control: { type: 'radio' },
options: [undefined, ...Object.values(AvatarStoryRing)],
options: [undefined, ...Object.values(HasStories)],
},
theme: {
control: { type: 'radio' },
@ -263,7 +263,7 @@ BlurredWithClickToView.story = {
export const StoryUnread = TemplateSingle.bind({});
StoryUnread.args = createProps({
avatarPath: '/fixtures/kitten-3-64-64.jpg',
storyRing: AvatarStoryRing.Unread,
storyRing: HasStories.Unread,
});
StoryUnread.story = {
name: 'Story: unread',
@ -272,7 +272,7 @@ StoryUnread.story = {
export const StoryRead = TemplateSingle.bind({});
StoryRead.args = createProps({
avatarPath: '/fixtures/kitten-3-64-64.jpg',
storyRing: AvatarStoryRing.Read,
storyRing: HasStories.Read,
});
StoryRead.story = {
name: 'Story: read',

View File

@ -12,19 +12,19 @@ import React, { useEffect, useState } from 'react';
import classNames from 'classnames';
import { noop } from 'lodash';
import { Spinner } from './Spinner';
import { getInitials } from '../util/getInitials';
import type { LocalizerType } from '../types/Util';
import { ThemeType } from '../types/Util';
import type { AvatarColorType } from '../types/Colors';
import type { BadgeType } from '../badges/types';
import type { LocalizerType } from '../types/Util';
import * as log from '../logging/log';
import { assert } from '../util/assert';
import { shouldBlurAvatar } from '../util/shouldBlurAvatar';
import { getBadgeImageFileLocalPath } from '../badges/getBadgeImageFileLocalPath';
import { isBadgeVisible } from '../badges/isBadgeVisible';
import { BadgeImageTheme } from '../badges/BadgeImageTheme';
import { HasStories } from '../types/Stories';
import { Spinner } from './Spinner';
import { ThemeType } from '../types/Util';
import { assert } from '../util/assert';
import { getBadgeImageFileLocalPath } from '../badges/getBadgeImageFileLocalPath';
import { getInitials } from '../util/getInitials';
import { isBadgeVisible } from '../badges/isBadgeVisible';
import { shouldBlurAvatar } from '../util/shouldBlurAvatar';
import { shouldShowBadges } from '../badges/shouldShowBadges';
export enum AvatarBlur {
@ -45,11 +45,6 @@ export enum AvatarSize {
ONE_HUNDRED_TWELVE = 112,
}
export enum AvatarStoryRing {
Unread = 'Unread',
Read = 'Read',
}
type BadgePlacementType = { bottom: number; right: number };
export type Props = {
@ -70,7 +65,7 @@ export type Props = {
title: string;
unblurredAvatarPath?: string;
searchResult?: boolean;
storyRing?: AvatarStoryRing;
storyRing?: HasStories;
onClick?: (event: MouseEvent<HTMLButtonElement>) => unknown;
onClickBadge?: (event: MouseEvent<HTMLButtonElement>) => unknown;
@ -308,9 +303,8 @@ export const Avatar: FunctionComponent<Props> = ({
className={classNames(
'module-Avatar',
hasImage ? 'module-Avatar--with-image' : 'module-Avatar--no-image',
storyRing && 'module-Avatar--with-story',
storyRing === AvatarStoryRing.Unread &&
'module-Avatar--with-story--unread',
Boolean(storyRing) && 'module-Avatar--with-story',
storyRing === HasStories.Unread && 'module-Avatar--with-story--unread',
className
)}
style={{

View File

@ -5,9 +5,10 @@ import React, { useState } from 'react';
import classNames from 'classnames';
import type { LocalizerType } from '../types/Util';
import type { ConversationStoryType, StoryViewType } from '../types/Stories';
import { Avatar, AvatarSize, AvatarStoryRing } from './Avatar';
import { Avatar, AvatarSize } from './Avatar';
import { ConfirmationDialog } from './ConfirmationDialog';
import { ContextMenuPopper } from './ContextMenu';
import { HasStories } from '../types/Stories';
import { MessageTimestamp } from './conversation/MessageTimestamp';
import { StoryImage } from './StoryImage';
import { getAvatarColor } from '../types/Colors';
@ -57,9 +58,9 @@ export const StoryListItem = ({
title,
} = sender;
let avatarStoryRing: AvatarStoryRing | undefined;
let avatarStoryRing: HasStories | undefined;
if (attachment) {
avatarStoryRing = isUnread ? AvatarStoryRing.Unread : AvatarStoryRing.Read;
avatarStoryRing = isUnread ? HasStories.Unread : HasStories.Read;
}
let repliesElement: JSX.Element | undefined;

View File

@ -1,147 +1,147 @@
// Copyright 2020-2021 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Meta, Story } from '@storybook/react';
import * as React from 'react';
import casual from 'casual';
import { action } from '@storybook/addon-actions';
import { boolean } from '@storybook/addon-knobs';
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
import type { PropsType } from './ContactModal';
import { ContactModal } from './ContactModal';
import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
import type { ConversationType } from '../../state/ducks/conversations';
import { getFakeBadges } from '../../test-both/helpers/getFakeBadge';
import type { PropsType } from './ContactModal';
import enMessages from '../../../_locales/en/messages.json';
import { ContactModal } from './ContactModal';
import { HasStories } from '../../types/Stories';
import { ThemeType } from '../../types/Util';
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
import { getFakeBadges } from '../../test-both/helpers/getFakeBadge';
import { setupI18n } from '../../util/setupI18n';
const i18n = setupI18n('en', enMessages);
export default {
title: 'Components/Conversation/ContactModal',
};
const defaultContact: ConversationType = getDefaultConversation({
id: 'abcdef',
title: 'Pauline Oliveros',
phoneNumber: '(333) 444-5515',
about: '👍 Free to chat',
});
const defaultGroup: ConversationType = getDefaultConversation({
id: 'abcdef',
areWeAdmin: true,
title: "It's a group",
groupLink: 'something',
groupLink: casual.url,
title: casual.title,
type: 'group',
});
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
areWeASubscriber: false,
areWeAdmin: boolean('areWeAdmin', overrideProps.areWeAdmin || false),
badges: overrideProps.badges || [],
contact: overrideProps.contact || defaultContact,
conversation: overrideProps.conversation || defaultGroup,
hideContactModal: action('hideContactModal'),
i18n,
isAdmin: boolean('isAdmin', overrideProps.isAdmin || false),
isMember: boolean('isMember', overrideProps.isMember || true),
removeMemberFromGroup: action('removeMemberFromGroup'),
showConversation: action('showConversation'),
theme: ThemeType.light,
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),
toggleAdmin: action('toggleAdmin'),
updateConversationModelSharedGroups: action(
'updateConversationModelSharedGroups'
),
});
export default {
title: 'Components/Conversation/ContactModal',
component: ContactModal,
argTypes: {
i18n: {
defaultValue: i18n,
},
areWeASubscriber: {
defaultValue: false,
},
areWeAdmin: {
defaultValue: false,
},
badges: {
defaultValue: [],
},
contact: {
defaultValue: defaultContact,
},
conversation: {
defaultValue: defaultGroup,
},
hasStories: {
defaultValue: undefined,
},
hideContactModal: { action: true },
isAdmin: {
defaultValue: false,
},
isMember: {
defaultValue: true,
},
removeMemberFromGroup: { action: true },
showConversation: { action: true },
theme: {
defaultValue: ThemeType.light,
},
toggleAdmin: { action: true },
toggleSafetyNumberModal: { action: true },
updateConversationModelSharedGroups: { action: true },
viewUserStories: { action: true },
},
} as Meta;
export const AsNonAdmin = (): JSX.Element => {
const props = createProps({
areWeAdmin: false,
});
const Template: Story<PropsType> = args => <ContactModal {...args} />;
return <ContactModal {...props} />;
export const AsNonAdmin = Template.bind({});
AsNonAdmin.args = {
areWeAdmin: false,
};
AsNonAdmin.story = {
name: 'As non-admin',
};
export const AsAdmin = (): JSX.Element => {
const props = createProps({
areWeAdmin: true,
});
return <ContactModal {...props} />;
export const AsAdmin = Template.bind({});
AsAdmin.args = {
areWeAdmin: true,
};
AsAdmin.story = {
name: 'As admin',
};
export const AsAdminWithNoGroupLink = (): JSX.Element => {
const props = createProps({
areWeAdmin: true,
conversation: {
...defaultGroup,
groupLink: undefined,
},
});
return <ContactModal {...props} />;
export const AsAdminWithNoGroupLink = Template.bind({});
AsAdminWithNoGroupLink.args = {
areWeAdmin: true,
conversation: {
...defaultGroup,
groupLink: undefined,
},
};
AsAdminWithNoGroupLink.story = {
name: 'As admin with no group link',
};
export const AsAdminViewingNonMemberOfGroup = (): JSX.Element => {
const props = createProps({
isMember: false,
});
return <ContactModal {...props} />;
export const AsAdminViewingNonMemberOfGroup = Template.bind({});
AsAdminViewingNonMemberOfGroup.args = {
isMember: false,
};
AsAdminViewingNonMemberOfGroup.story = {
name: 'As admin, viewing non-member of group',
};
export const WithoutPhoneNumber = (): JSX.Element => {
const props = createProps({
contact: {
...defaultContact,
phoneNumber: undefined,
},
});
return <ContactModal {...props} />;
export const WithoutPhoneNumber = Template.bind({});
WithoutPhoneNumber.args = {
contact: {
...defaultContact,
phoneNumber: undefined,
},
};
WithoutPhoneNumber.story = {
name: 'Without phone number',
};
export const ViewingSelf = (): JSX.Element => {
const props = createProps({
contact: {
...defaultContact,
isMe: true,
},
});
return <ContactModal {...props} />;
export const ViewingSelf = Template.bind({});
ViewingSelf.args = {
contact: {
...defaultContact,
isMe: true,
},
};
ViewingSelf.story = {
name: 'Viewing self',
};
export const WithBadges = (): JSX.Element => {
const props = createProps({
badges: getFakeBadges(2),
});
return <ContactModal {...props} />;
export const WithBadges = Template.bind({});
WithBadges.args = {
badges: getFakeBadges(2),
};
WithBadges.story = {
name: 'With badges',
};
export const WithUnreadStories = Template.bind({});
WithUnreadStories.args = {
hasStories: HasStories.Unread,
};
WithUnreadStories.storyName = 'Unread Stories';

View File

@ -1,25 +1,26 @@
// Copyright 2020-2021 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect, useState } from 'react';
import type { ReactNode } from 'react';
import * as log from '../../logging/log';
import { missingCaseError } from '../../util/missingCaseError';
import { About } from './About';
import { Avatar } from '../Avatar';
import { AvatarLightbox } from '../AvatarLightbox';
import type {
ConversationType,
ShowConversationType,
} from '../../state/ducks/conversations';
import { Modal } from '../Modal';
import type { LocalizerType, ThemeType } from '../../types/Util';
import { BadgeDialog } from '../BadgeDialog';
import type { BadgeType } from '../../badges/types';
import { SharedGroupNames } from '../SharedGroupNames';
import type { HasStories } from '../../types/Stories';
import type { LocalizerType, ThemeType } from '../../types/Util';
import * as log from '../../logging/log';
import { About } from './About';
import { Avatar } from '../Avatar';
import { AvatarLightbox } from '../AvatarLightbox';
import { BadgeDialog } from '../BadgeDialog';
import { ConfirmationDialog } from '../ConfirmationDialog';
import { Modal } from '../Modal';
import { RemoveGroupMemberConfirmationDialog } from './RemoveGroupMemberConfirmationDialog';
import { SharedGroupNames } from '../SharedGroupNames';
import { missingCaseError } from '../../util/missingCaseError';
export type PropsDataType = {
areWeASubscriber: boolean;
@ -27,6 +28,7 @@ export type PropsDataType = {
badges: ReadonlyArray<BadgeType>;
contact?: ConversationType;
conversation?: ConversationType;
hasStories?: HasStories;
readonly i18n: LocalizerType;
isAdmin: boolean;
isMember: boolean;
@ -40,6 +42,7 @@ type PropsActionType = {
toggleAdmin: (conversationId: string, contactId: string) => void;
toggleSafetyNumberModal: (conversationId: string) => unknown;
updateConversationModelSharedGroups: (conversationId: string) => void;
viewUserStories: (cid: string) => unknown;
};
export type PropsType = PropsDataType & PropsActionType;
@ -62,6 +65,7 @@ export const ContactModal = ({
badges,
contact,
conversation,
hasStories,
hideContactModal,
i18n,
isAdmin,
@ -72,6 +76,7 @@ export const ContactModal = ({
toggleAdmin,
toggleSafetyNumberModal,
updateConversationModelSharedGroups,
viewUserStories,
}: PropsType): JSX.Element => {
if (!contact) {
throw new Error('Contact modal opened without a matching contact');
@ -172,14 +177,21 @@ export const ContactModal = ({
i18n={i18n}
isMe={contact.isMe}
name={contact.name}
onClick={() => {
if (conversation && hasStories) {
viewUserStories(conversation.id);
} else {
setView(ContactModalView.ShowingAvatar);
}
}}
onClickBadge={() => setView(ContactModalView.ShowingBadges)}
profileName={contact.profileName}
sharedGroupNames={contact.sharedGroupNames}
size={96}
storyRing={hasStories}
theme={theme}
title={contact.title}
unblurredAvatarPath={contact.unblurredAvatarPath}
onClick={() => setView(ContactModalView.ShowingAvatar)}
onClickBadge={() => setView(ContactModalView.ShowingBadges)}
/>
<div className="ContactModal__name">{contact.title}</div>
<div className="module-about__container">

View File

@ -1,4 +1,4 @@
// Copyright 2020-2021 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ComponentProps } from 'react';
@ -56,6 +56,7 @@ const commonProps = {
onMarkUnread: action('onMarkUnread'),
onMoveToInbox: action('onMoveToInbox'),
onSetPin: action('onSetPin'),
viewUserStories: action('viewUserStories'),
};
export const PrivateConvo = (): JSX.Element => {

View File

@ -1,4 +1,4 @@
// Copyright 2018-2021 Signal Messenger, LLC
// Copyright 2018-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react';
@ -20,6 +20,7 @@ import { InContactsIcon } from '../InContactsIcon';
import type { LocalizerType, ThemeType } from '../../types/Util';
import type { ConversationType } from '../../state/ducks/conversations';
import type { BadgeType } from '../../badges/types';
import type { HasStories } from '../../types/Stories';
import { getMuteOptions } from '../../util/getMuteOptions';
import * as expirationTimer from '../../util/expirationTimer';
import { missingCaseError } from '../../util/missingCaseError';
@ -39,6 +40,7 @@ export enum OutgoingCallButtonStyle {
export type PropsDataType = {
badge?: BadgeType;
conversationTitle?: string;
hasStories?: HasStories;
isMissingMandatoryProfileSharing?: boolean;
outgoingCallButtonStyle: OutgoingCallButtonStyle;
showBackButton?: boolean;
@ -88,6 +90,7 @@ export type PropsActionsType = {
onArchive: () => void;
onMarkUnread: () => void;
onMoveToInbox: () => void;
viewUserStories: (cid: string) => unknown;
};
export type PropsHousekeepingType = {
@ -199,6 +202,8 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
avatarPath,
badge,
color,
hasStories,
id,
i18n,
type,
isMe,
@ -209,6 +214,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
theme,
title,
unblurredAvatarPath,
viewUserStories,
} = this.props;
return (
@ -221,14 +227,22 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
conversationType={type}
i18n={i18n}
isMe={isMe}
noteToSelf={isMe}
title={title}
name={name}
noteToSelf={isMe}
onClick={
hasStories
? () => {
viewUserStories(id);
}
: undefined
}
phoneNumber={phoneNumber}
profileName={profileName}
sharedGroupNames={sharedGroupNames}
size={AvatarSize.THIRTY_TWO}
storyRing={hasStories}
theme={theme}
title={title}
unblurredAvatarPath={unblurredAvatarPath}
/>
</span>
@ -488,30 +502,32 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
throw missingCaseError(type);
}
const avatar = this.renderAvatar();
const contents = (
<>
{this.renderAvatar()}
<div className="module-ConversationHeader__header__info">
{this.renderHeaderInfoTitle()}
{this.renderHeaderInfoSubtitle()}
</div>
</>
<div className="module-ConversationHeader__header__info">
{this.renderHeaderInfoTitle()}
{this.renderHeaderInfoSubtitle()}
</div>
);
if (onClick) {
return (
<button
type="button"
className="module-ConversationHeader__header module-ConversationHeader__header--clickable"
onClick={onClick}
>
{contents}
</button>
<div className="module-ConversationHeader__header">
{avatar}
<button
type="button"
className="module-ConversationHeader__header--clickable"
onClick={onClick}
>
{contents}
</button>
</div>
);
}
return (
<div className="module-ConversationHeader__header" ref={this.headerRef}>
{avatar}
{contents}
</div>
);

View File

@ -1,458 +1,213 @@
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { number as numberKnob, text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import type { Meta, Story } from '@storybook/react';
import React, { useContext } from 'react';
import casual from 'casual';
import { ConversationHero } from './ConversationHero';
import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
import type { Props } from './ConversationHero';
import { ConversationHero } from './ConversationHero';
import { HasStories } from '../../types/Stories';
import { StorybookThemeContext } from '../../../.storybook/StorybookThemeContext';
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
import { setupI18n } from '../../util/setupI18n';
import { ThemeType } from '../../types/Util';
const i18n = setupI18n('en', enMessages);
const getAbout = () => text('about', '👍 Free to chat');
const getTitle = () => text('name', 'Cayce Bollard');
const getName = () => text('name', 'Cayce Bollard');
const getProfileName = () => text('profileName', 'Cayce Bollard (profile)');
const getAvatarPath = () =>
text('avatarPath', '/fixtures/kitten-4-112-112.jpg');
const getPhoneNumber = () => text('phoneNumber', '+1 (646) 327-2700');
const updateSharedGroups = action('updateSharedGroups');
const Wrapper = (
props: Omit<React.ComponentProps<typeof ConversationHero>, 'theme'>
) => {
const theme = React.useContext(StorybookThemeContext);
return <ConversationHero {...props} theme={theme} />;
};
export default {
title: 'Components/Conversation/ConversationHero',
};
component: ConversationHero,
argTypes: {
conversationType: {
defaultValue: 'direct',
},
i18n: {
defaultValue: i18n,
},
theme: {
defaultValue: ThemeType.light,
},
unblurAvatar: { action: true },
updateSharedGroups: { action: true },
viewUserStories: { action: true },
},
} as Meta;
export const DirectFiveOtherGroups = (): JSX.Element => {
const Template: Story<Props> = args => {
const theme = useContext(StorybookThemeContext);
return (
<div style={{ width: '480px' }}>
<Wrapper
about={getAbout()}
acceptedMessageRequest
badge={undefined}
i18n={i18n}
isMe={false}
title={getTitle()}
avatarPath={getAvatarPath()}
name={getName()}
profileName={getProfileName()}
phoneNumber={getPhoneNumber()}
conversationType="direct"
updateSharedGroups={updateSharedGroups}
sharedGroupNames={[
'NYC Rock Climbers',
'Dinner Party',
'Friends 🌿',
'Fourth',
'Fifth',
]}
unblurAvatar={action('unblurAvatar')}
/>
<ConversationHero {...getDefaultConversation()} {...args} theme={theme} />
</div>
);
};
export const DirectFiveOtherGroups = Template.bind({});
DirectFiveOtherGroups.args = {
sharedGroupNames: Array.from(Array(5), () => casual.title),
};
DirectFiveOtherGroups.story = {
name: 'Direct (Five Other Groups)',
};
export const DirectFourOtherGroups = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
about={getAbout()}
acceptedMessageRequest
badge={undefined}
i18n={i18n}
isMe={false}
title={getTitle()}
avatarPath={getAvatarPath()}
name={getName()}
profileName={getProfileName()}
phoneNumber={getPhoneNumber()}
conversationType="direct"
updateSharedGroups={updateSharedGroups}
sharedGroupNames={[
'NYC Rock Climbers',
'Dinner Party',
'Friends 🌿',
'Fourth',
]}
unblurAvatar={action('unblurAvatar')}
/>
</div>
);
export const DirectFourOtherGroups = Template.bind({});
DirectFourOtherGroups.args = {
sharedGroupNames: Array.from(Array(4), () => casual.title),
};
DirectFourOtherGroups.story = {
name: 'Direct (Four Other Groups)',
};
export const DirectThreeOtherGroups = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
about={getAbout()}
acceptedMessageRequest
badge={undefined}
i18n={i18n}
isMe={false}
title={getTitle()}
avatarPath={getAvatarPath()}
name={getName()}
profileName={getProfileName()}
phoneNumber={getPhoneNumber()}
conversationType="direct"
updateSharedGroups={updateSharedGroups}
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party', 'Friends 🌿']}
unblurAvatar={action('unblurAvatar')}
/>
</div>
);
export const DirectThreeOtherGroups = Template.bind({});
DirectThreeOtherGroups.args = {
sharedGroupNames: Array.from(Array(3), () => casual.title),
};
DirectThreeOtherGroups.story = {
name: 'Direct (Three Other Groups)',
};
export const DirectTwoOtherGroups = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
about={getAbout()}
acceptedMessageRequest
badge={undefined}
i18n={i18n}
isMe={false}
title={getTitle()}
avatarPath={getAvatarPath()}
name={getName()}
profileName={getProfileName()}
phoneNumber={getPhoneNumber()}
conversationType="direct"
updateSharedGroups={updateSharedGroups}
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
unblurAvatar={action('unblurAvatar')}
/>
</div>
);
export const DirectTwoOtherGroups = Template.bind({});
DirectTwoOtherGroups.args = {
sharedGroupNames: Array.from(Array(2), () => casual.title),
};
DirectTwoOtherGroups.story = {
name: 'Direct (Two Other Groups)',
};
export const DirectOneOtherGroup = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
about={getAbout()}
acceptedMessageRequest
badge={undefined}
i18n={i18n}
isMe={false}
title={getTitle()}
avatarPath={getAvatarPath()}
name={getName()}
profileName={getProfileName()}
phoneNumber={getPhoneNumber()}
conversationType="direct"
updateSharedGroups={updateSharedGroups}
sharedGroupNames={['NYC Rock Climbers']}
unblurAvatar={action('unblurAvatar')}
/>
</div>
);
export const DirectOneOtherGroup = Template.bind({});
DirectOneOtherGroup.args = {
sharedGroupNames: [casual.title],
};
DirectOneOtherGroup.story = {
name: 'Direct (One Other Group)',
};
export const DirectNoGroupsName = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
about={getAbout()}
acceptedMessageRequest
badge={undefined}
i18n={i18n}
isMe={false}
title={getTitle()}
avatarPath={getAvatarPath()}
name={getName()}
profileName={text('profileName', '')}
phoneNumber={getPhoneNumber()}
conversationType="direct"
updateSharedGroups={updateSharedGroups}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
/>
</div>
);
export const DirectNoGroupsName = Template.bind({});
DirectNoGroupsName.args = {
about: '👍 Free to chat',
};
DirectNoGroupsName.story = {
name: 'Direct (No Groups, Name)',
};
export const DirectNoGroupsJustProfile = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
about={getAbout()}
acceptedMessageRequest
badge={undefined}
i18n={i18n}
isMe={false}
title={text('title', 'Cayce Bollard (profile)')}
avatarPath={getAvatarPath()}
name={text('name', '')}
profileName={getProfileName()}
phoneNumber={getPhoneNumber()}
conversationType="direct"
updateSharedGroups={updateSharedGroups}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
/>
</div>
);
export const DirectNoGroupsJustProfile = Template.bind({});
DirectNoGroupsJustProfile.args = {
phoneNumber: casual.phone,
};
DirectNoGroupsJustProfile.story = {
name: 'Direct (No Groups, Just Profile)',
};
export const DirectNoGroupsJustPhoneNumber = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
about={getAbout()}
acceptedMessageRequest
badge={undefined}
i18n={i18n}
isMe={false}
title={text('title', '+1 (646) 327-2700')}
avatarPath={getAvatarPath()}
name={text('name', '')}
profileName={text('profileName', '')}
phoneNumber={getPhoneNumber()}
conversationType="direct"
updateSharedGroups={updateSharedGroups}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
/>
</div>
);
export const DirectNoGroupsJustPhoneNumber = Template.bind({});
DirectNoGroupsJustPhoneNumber.args = {
name: '',
phoneNumber: casual.phone,
profileName: '',
title: '',
};
DirectNoGroupsJustPhoneNumber.story = {
name: 'Direct (No Groups, Just Phone Number)',
};
export const DirectNoGroupsNoData = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
i18n={i18n}
isMe={false}
title={text('title', 'Unknown contact')}
acceptedMessageRequest
badge={undefined}
avatarPath={getAvatarPath()}
name={text('name', '')}
profileName={text('profileName', '')}
phoneNumber={text('phoneNumber', '')}
conversationType="direct"
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);
export const DirectNoGroupsNoData = Template.bind({});
DirectNoGroupsNoData.args = {
avatarPath: undefined,
name: '',
phoneNumber: '',
profileName: '',
title: '',
};
DirectNoGroupsNoData.story = {
name: 'Direct (No Groups, No Data)',
};
export const DirectNoGroupsNoDataNotAccepted = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
i18n={i18n}
isMe={false}
title={text('title', 'Unknown contact')}
acceptedMessageRequest={false}
badge={undefined}
avatarPath={getAvatarPath()}
name={text('name', '')}
profileName={text('profileName', '')}
phoneNumber={text('phoneNumber', '')}
conversationType="direct"
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);
export const DirectNoGroupsNoDataNotAccepted = Template.bind({});
DirectNoGroupsNoDataNotAccepted.args = {
acceptedMessageRequest: false,
avatarPath: undefined,
name: '',
phoneNumber: '',
profileName: '',
title: '',
};
DirectNoGroupsNoDataNotAccepted.story = {
name: 'Direct (No Groups, No Data, Not Accepted)',
};
export const GroupManyMembers = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
acceptedMessageRequest
badge={undefined}
i18n={i18n}
isMe={false}
title={text('title', 'NYC Rock Climbers')}
name={text('groupName', 'NYC Rock Climbers')}
conversationType="group"
membersCount={numberKnob('membersCount', 22)}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);
export const GroupManyMembers = Template.bind({});
GroupManyMembers.args = {
conversationType: 'group',
groupDescription: casual.sentence,
membersCount: casual.integer(20, 100),
title: casual.title,
};
GroupManyMembers.story = {
name: 'Group (many members)',
};
export const GroupOneMember = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
acceptedMessageRequest
badge={undefined}
i18n={i18n}
isMe={false}
title={text('title', 'NYC Rock Climbers')}
name={text('groupName', 'NYC Rock Climbers')}
conversationType="group"
membersCount={1}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);
export const GroupOneMember = Template.bind({});
GroupOneMember.args = {
avatarPath: undefined,
conversationType: 'group',
groupDescription: casual.sentence,
membersCount: 1,
title: casual.title,
};
GroupOneMember.story = {
name: 'Group (one member)',
};
export const GroupZeroMembers = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
acceptedMessageRequest
badge={undefined}
i18n={i18n}
isMe={false}
title={text('title', 'NYC Rock Climbers')}
name={text('groupName', 'NYC Rock Climbers')}
conversationType="group"
groupDescription="This is a group for all the rock climbers of NYC"
membersCount={0}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);
export const GroupZeroMembers = Template.bind({});
GroupZeroMembers.args = {
avatarPath: undefined,
conversationType: 'group',
groupDescription: casual.sentence,
membersCount: 0,
title: casual.title,
};
GroupZeroMembers.story = {
name: 'Group (zero members)',
};
export const GroupLongGroupDescription = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
acceptedMessageRequest
badge={undefined}
i18n={i18n}
isMe={false}
title={text('title', 'NYC Rock Climbers')}
name={text('groupName', 'NYC Rock Climbers')}
conversationType="group"
groupDescription="This is a group for all the rock climbers of NYC. We really like to climb rocks and these NYC people climb any rock. No rock is too small or too big to be climbed. We will ascend upon all rocks, and not just in NYC, in the whole world. We are just getting started, NYC is just the beginning, watch out rocks in the galaxy. Kuiper belt I'm looking at you. We will put on a space suit and climb all your rocks. No rock is near nor far for the rock climbers of NYC."
membersCount={0}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);
export const GroupLongGroupDescription = Template.bind({});
GroupLongGroupDescription.args = {
conversationType: 'group',
groupDescription:
"This is a group for all the rock climbers of NYC. We really like to climb rocks and these NYC people climb any rock. No rock is too small or too big to be climbed. We will ascend upon all rocks, and not just in NYC, in the whole world. We are just getting started, NYC is just the beginning, watch out rocks in the galaxy. Kuiper belt I'm looking at you. We will put on a space suit and climb all your rocks. No rock is near nor far for the rock climbers of NYC.",
membersCount: casual.integer(1, 10),
title: casual.title,
};
GroupLongGroupDescription.story = {
name: 'Group (long group description)',
};
export const GroupNoName = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
acceptedMessageRequest
badge={undefined}
i18n={i18n}
isMe={false}
title={text('title', 'Unknown group')}
name={text('groupName', '')}
conversationType="group"
membersCount={0}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);
export const GroupNoName = Template.bind({});
GroupNoName.args = {
conversationType: 'group',
membersCount: 0,
name: '',
title: '',
};
GroupNoName.story = {
name: 'Group (No name)',
};
export const NoteToSelf = (): JSX.Element => {
return (
<div style={{ width: '480px' }}>
<Wrapper
acceptedMessageRequest
badge={undefined}
i18n={i18n}
isMe
title={getTitle()}
conversationType="direct"
phoneNumber={getPhoneNumber()}
sharedGroupNames={[]}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={updateSharedGroups}
/>
</div>
);
export const NoteToSelf = Template.bind({});
NoteToSelf.args = {
isMe: true,
};
NoteToSelf.story = {
name: 'Note to Self',
};
export const UnreadStories = Template.bind({});
UnreadStories.args = {
hasStories: HasStories.Unread,
};
export const ReadStories = Template.bind({});
ReadStories.args = {
hasStories: HasStories.Read,
};

View File

@ -9,6 +9,7 @@ import { About } from './About';
import { GroupDescription } from './GroupDescription';
import { SharedGroupNames } from '../SharedGroupNames';
import type { LocalizerType, ThemeType } from '../../types/Util';
import type { HasStories } from '../../types/Stories';
import { ConfirmationDialog } from '../ConfirmationDialog';
import { Button, ButtonSize, ButtonVariant } from '../Button';
import { shouldBlurAvatar } from '../../util/shouldBlurAvatar';
@ -18,6 +19,8 @@ export type Props = {
about?: string;
acceptedMessageRequest?: boolean;
groupDescription?: string;
hasStories?: HasStories;
id: string;
i18n: LocalizerType;
isMe: boolean;
membersCount?: number;
@ -27,6 +30,7 @@ export type Props = {
unblurredAvatarPath?: string;
updateSharedGroups: () => unknown;
theme: ThemeType;
viewUserStories: (cid: string) => unknown;
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
const renderMembershipRow = ({
@ -101,6 +105,8 @@ export const ConversationHero = ({
color,
conversationType,
groupDescription,
hasStories,
id,
isMe,
membersCount,
sharedGroupNames = [],
@ -112,6 +118,7 @@ export const ConversationHero = ({
unblurAvatar,
unblurredAvatarPath,
updateSharedGroups,
viewUserStories,
}: Props): JSX.Element => {
const [isShowingMessageRequestWarning, setIsShowingMessageRequestWarning] =
useState(false);
@ -124,7 +131,7 @@ export const ConversationHero = ({
updateSharedGroups();
}, [updateSharedGroups]);
let avatarBlur: AvatarBlur;
let avatarBlur: AvatarBlur = AvatarBlur.NoBlur;
let avatarOnClick: undefined | (() => void);
if (
shouldBlurAvatar({
@ -137,8 +144,10 @@ export const ConversationHero = ({
) {
avatarBlur = AvatarBlur.BlurPictureWithClickToView;
avatarOnClick = unblurAvatar;
} else {
avatarBlur = AvatarBlur.NoBlur;
} else if (hasStories) {
avatarOnClick = () => {
viewUserStories(id);
};
}
const phoneNumberOnly = Boolean(
@ -165,6 +174,7 @@ export const ConversationHero = ({
profileName={profileName}
sharedGroupNames={sharedGroupNames}
size={112}
storyRing={hasStories}
theme={theme}
title={title}
/>

View File

@ -491,19 +491,21 @@ const renderHeroRow = () => {
<ConversationHero
about={getAbout()}
acceptedMessageRequest
avatarPath={getAvatarPath()}
badge={undefined}
conversationType="direct"
id={getDefaultConversation().id}
i18n={i18n}
isMe={false}
title={getTitle()}
avatarPath={getAvatarPath()}
name={getName()}
profileName={getProfileName()}
phoneNumber={getPhoneNumber()}
conversationType="direct"
profileName={getProfileName()}
sharedGroupNames={['NYC Rock Climbers', 'Dinner Party']}
theme={theme}
title={getTitle()}
unblurAvatar={action('unblurAvatar')}
updateSharedGroups={noop}
viewUserStories={action('viewUserStories')}
/>
);
};

View File

@ -20,7 +20,7 @@ import type {
StoryDataType,
StoriesStateType,
} from '../ducks/stories';
import { MY_STORIES_ID } from '../../types/Stories';
import { HasStories, MY_STORIES_ID } from '../../types/Stories';
import { ReadStatus } from '../../messages/MessageReadStatus';
import { SendStatus } from '../../messages/MessageSendState';
import { canReply } from './message';
@ -30,6 +30,7 @@ import {
getMe,
} from './conversations';
import { getDistributionListSelector } from './storyDistributionLists';
import { getStoriesEnabled } from './items';
export const getStoriesState = (state: StateType): StoriesStateType =>
state.stories;
@ -348,3 +349,28 @@ export const getUnreadStoriesCount = createSelector(
.length;
}
);
export const getHasStoriesSelector = createSelector(
getStoriesEnabled,
getStoriesState,
(isEnabled, { stories }) =>
(conversationId?: string): HasStories | undefined => {
if (!isEnabled || !conversationId) {
return;
}
const conversationStories = stories.filter(
story => story.conversationId === conversationId
);
if (!conversationStories.length) {
return;
}
return conversationStories.some(
story => story.readStatus === ReadStatus.Unread
)
? HasStories.Unread
: HasStories.Read;
}
);

View File

@ -11,6 +11,7 @@ import { getAreWeASubscriber } from '../selectors/items';
import { getIntl, getTheme } from '../selectors/user';
import { getBadgesSelector } from '../selectors/badges';
import { getConversationSelector } from '../selectors/conversations';
import { getHasStoriesSelector } from '../selectors/stories';
const mapStateToProps = (state: StateType): PropsDataType => {
const { contactId, conversationId } =
@ -35,12 +36,15 @@ const mapStateToProps = (state: StateType): PropsDataType => {
});
}
const hasStories = getHasStoriesSelector(state)(conversationId);
return {
areWeASubscriber: getAreWeASubscriber(state),
areWeAdmin,
badges: getBadgesSelector(state)(contact.badges),
contact,
conversation: currentConversation,
hasStories,
i18n: getIntl(state),
isAdmin,
isMember,

View File

@ -3,6 +3,8 @@
import { connect } from 'react-redux';
import { pick } from 'lodash';
import type { ConversationType } from '../ducks/conversations';
import type { StateType } from '../reducer';
import {
ConversationHeader,
OutgoingCallButtonStyle,
@ -12,15 +14,15 @@ import {
getConversationSelector,
isMissingRequiredProfileSharing,
} from '../selectors/conversations';
import type { StateType } from '../reducer';
import { CallMode } from '../../types/Calling';
import type { ConversationType } from '../ducks/conversations';
import { getConversationCallMode } from '../ducks/conversations';
import { getActiveCall, isAnybodyElseInGroupCall } from '../ducks/calling';
import { getUserACI, getIntl, getTheme } from '../selectors/user';
import { getConversationCallMode } from '../ducks/conversations';
import { getHasStoriesSelector } from '../selectors/stories';
import { getOwn } from '../../util/getOwn';
import { missingCaseError } from '../../util/missingCaseError';
import { getUserACI, getIntl, getTheme } from '../selectors/user';
import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
import { mapDispatchToProps } from '../actions';
import { missingCaseError } from '../../util/missingCaseError';
import { strictAssert } from '../../util/assert';
export type OwnProps = {
@ -83,6 +85,8 @@ const mapStateToProps = (state: StateType, ownProps: OwnProps) => {
throw new Error('Could not find conversation');
}
const hasStories = getHasStoriesSelector(state)(id);
return {
...pick(conversation, [
'acceptedMessageRequest',
@ -110,6 +114,7 @@ const mapStateToProps = (state: StateType, ownProps: OwnProps) => {
]),
badge: getPreferredBadgeSelector(state)(conversation.badges),
conversationTitle: state.conversations.selectedConversationTitle,
hasStories,
isMissingMandatoryProfileSharing:
isMissingRequiredProfileSharing(conversation),
isSMSOnly: isConversationSMSOnly(conversation),
@ -120,6 +125,6 @@ const mapStateToProps = (state: StateType, ownProps: OwnProps) => {
};
};
const smart = connect(mapStateToProps, {});
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartConversationHeader = smart(ConversationHeader);

View File

@ -9,6 +9,7 @@ import { ConversationHero } from '../../components/conversation/ConversationHero
import type { StateType } from '../reducer';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getIntl, getTheme } from '../selectors/user';
import { getHasStoriesSelector } from '../selectors/stories';
type ExternalProps = {
id: string;
@ -27,6 +28,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
i18n: getIntl(state),
...conversation,
conversationType: conversation.type,
hasStories: getHasStoriesSelector(state)(id),
badge: getPreferredBadgeSelector(state)(conversation.badges),
theme: getTheme(state),
};

View File

@ -127,3 +127,8 @@ export function getStoryDistributionListName(
): string {
return id === MY_STORIES_ID ? i18n('Stories__mine') : name;
}
export enum HasStories {
Read = 'Read',
Unread = 'Unread',
}