Add story entry points around the app
This commit is contained in:
parent
1d5b361159
commit
5dfe30d235
16 changed files with 367 additions and 533 deletions
|
@ -119,6 +119,7 @@
|
|||
margin-left: 4px;
|
||||
margin-right: var(--button-spacing);
|
||||
padding: $padding;
|
||||
padding-left: 0;
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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={{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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({
|
||||
const Template: Story<PropsType> = args => <ContactModal {...args} />;
|
||||
|
||||
export const AsNonAdmin = Template.bind({});
|
||||
AsNonAdmin.args = {
|
||||
areWeAdmin: false,
|
||||
});
|
||||
|
||||
return <ContactModal {...props} />;
|
||||
};
|
||||
|
||||
AsNonAdmin.story = {
|
||||
name: 'As non-admin',
|
||||
};
|
||||
|
||||
export const AsAdmin = (): JSX.Element => {
|
||||
const props = createProps({
|
||||
export const AsAdmin = Template.bind({});
|
||||
AsAdmin.args = {
|
||||
areWeAdmin: true,
|
||||
});
|
||||
return <ContactModal {...props} />;
|
||||
};
|
||||
|
||||
AsAdmin.story = {
|
||||
name: 'As admin',
|
||||
};
|
||||
|
||||
export const AsAdminWithNoGroupLink = (): JSX.Element => {
|
||||
const props = createProps({
|
||||
export const AsAdminWithNoGroupLink = Template.bind({});
|
||||
AsAdminWithNoGroupLink.args = {
|
||||
areWeAdmin: true,
|
||||
conversation: {
|
||||
...defaultGroup,
|
||||
groupLink: undefined,
|
||||
},
|
||||
});
|
||||
return <ContactModal {...props} />;
|
||||
};
|
||||
|
||||
AsAdminWithNoGroupLink.story = {
|
||||
name: 'As admin with no group link',
|
||||
};
|
||||
|
||||
export const AsAdminViewingNonMemberOfGroup = (): JSX.Element => {
|
||||
const props = createProps({
|
||||
export const AsAdminViewingNonMemberOfGroup = Template.bind({});
|
||||
AsAdminViewingNonMemberOfGroup.args = {
|
||||
isMember: false,
|
||||
});
|
||||
|
||||
return <ContactModal {...props} />;
|
||||
};
|
||||
|
||||
AsAdminViewingNonMemberOfGroup.story = {
|
||||
name: 'As admin, viewing non-member of group',
|
||||
};
|
||||
|
||||
export const WithoutPhoneNumber = (): JSX.Element => {
|
||||
const props = createProps({
|
||||
export const WithoutPhoneNumber = Template.bind({});
|
||||
WithoutPhoneNumber.args = {
|
||||
contact: {
|
||||
...defaultContact,
|
||||
phoneNumber: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return <ContactModal {...props} />;
|
||||
};
|
||||
|
||||
WithoutPhoneNumber.story = {
|
||||
name: 'Without phone number',
|
||||
};
|
||||
|
||||
export const ViewingSelf = (): JSX.Element => {
|
||||
const props = createProps({
|
||||
export const ViewingSelf = Template.bind({});
|
||||
ViewingSelf.args = {
|
||||
contact: {
|
||||
...defaultContact,
|
||||
isMe: true,
|
||||
},
|
||||
});
|
||||
|
||||
return <ContactModal {...props} />;
|
||||
};
|
||||
|
||||
ViewingSelf.story = {
|
||||
name: 'Viewing self',
|
||||
};
|
||||
|
||||
export const WithBadges = (): JSX.Element => {
|
||||
const props = createProps({
|
||||
export const WithBadges = Template.bind({});
|
||||
WithBadges.args = {
|
||||
badges: getFakeBadges(2),
|
||||
});
|
||||
|
||||
return <ContactModal {...props} />;
|
||||
};
|
||||
|
||||
WithBadges.story = {
|
||||
name: 'With badges',
|
||||
};
|
||||
|
||||
export const WithUnreadStories = Template.bind({});
|
||||
WithUnreadStories.args = {
|
||||
hasStories: HasStories.Unread,
|
||||
};
|
||||
WithUnreadStories.storyName = 'Unread Stories';
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<div className="module-ConversationHeader__header">
|
||||
{avatar}
|
||||
<button
|
||||
type="button"
|
||||
className="module-ConversationHeader__header module-ConversationHeader__header--clickable"
|
||||
className="module-ConversationHeader__header--clickable"
|
||||
onClick={onClick}
|
||||
>
|
||||
{contents}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-ConversationHeader__header" ref={this.headerRef}>
|
||||
{avatar}
|
||||
{contents}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -127,3 +127,8 @@ export function getStoryDistributionListName(
|
|||
): string {
|
||||
return id === MY_STORIES_ID ? i18n('Stories__mine') : name;
|
||||
}
|
||||
|
||||
export enum HasStories {
|
||||
Read = 'Read',
|
||||
Unread = 'Unread',
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue