Include sender in group update notifications

This commit is contained in:
Scott Nonnenberg 2020-03-26 14:47:35 -07:00
parent d88c21e5b6
commit 71436d18e2
28 changed files with 1016 additions and 472 deletions

View file

@ -2,11 +2,12 @@ import React from 'react';
import classNames from 'classnames';
import { getInitials } from '../util/getInitials';
import { LocalizerType } from '../types/Util';
import { ColorType, LocalizerType } from '../types/Util';
export interface Props {
avatarPath?: string;
color?: string;
color?: ColorType;
conversationType: 'group' | 'direct';
noteToSelf?: boolean;
name?: string;

View file

@ -4,13 +4,13 @@ import classNames from 'classnames';
import { Avatar } from './Avatar';
import { Emojify } from './conversation/Emojify';
import { LocalizerType } from '../types/Util';
import { ColorType, LocalizerType } from '../types/Util';
interface Props {
phoneNumber: string;
isMe?: boolean;
name?: string;
color: string;
color: ColorType;
verified: boolean;
profileName?: string;
avatarPath?: string;

View file

@ -8,12 +8,12 @@ import { ContactName } from './conversation/ContactName';
import { TypingAnimation } from './conversation/TypingAnimation';
import { cleanId } from './_util';
import { LocalizerType } from '../types/Util';
import { ColorType, LocalizerType } from '../types/Util';
export type PropsData = {
id: string;
phoneNumber: string;
color?: string;
color?: ColorType;
profileName?: string;
name?: string;
type: 'group' | 'direct';

View file

@ -2,13 +2,13 @@ import React from 'react';
import { LocalizerType, RenderTextCallbackType } from '../types/Util';
type FullJSX = Array<JSX.Element | string> | JSX.Element | string;
export type FullJSXType = Array<JSX.Element | string> | JSX.Element | string;
interface Props {
/** The translation string id */
id: string;
i18n: LocalizerType;
components?: Array<FullJSX>;
components?: Array<FullJSXType>;
renderText?: RenderTextCallbackType;
}
@ -19,7 +19,7 @@ export class Intl extends React.Component<Props> {
),
};
public getComponent(index: number, key: number): FullJSX | undefined {
public getComponent(index: number, key: number): FullJSXType | undefined {
const { id, components } = this.props;
if (!components || !components.length || components.length <= index) {

View file

@ -7,7 +7,7 @@ import { createPortal } from 'react-dom';
import { showSettings } from '../shims/Whisper';
import { Avatar } from './Avatar';
import { AvatarPopup } from './AvatarPopup';
import { LocalizerType } from '../types/Util';
import { ColorType, LocalizerType } from '../types/Util';
export interface PropsType {
searchTerm: string;
@ -25,7 +25,7 @@ export interface PropsType {
phoneNumber: string;
isMe: boolean;
name?: string;
color: string;
color: ColorType;
verified: boolean;
profileName?: string;
avatarPath?: string;

View file

@ -6,7 +6,7 @@ import { MessageBodyHighlight } from './MessageBodyHighlight';
import { Timestamp } from './conversation/Timestamp';
import { ContactName } from './conversation/ContactName';
import { LocalizerType } from '../types/Util';
import { ColorType, LocalizerType } from '../types/Util';
export type PropsDataType = {
isSelected?: boolean;
@ -22,7 +22,7 @@ export type PropsDataType = {
phoneNumber: string;
isMe?: boolean;
name?: string;
color?: string;
color?: ColorType;
profileName?: string;
avatarPath?: string;
};

View file

@ -50,7 +50,7 @@ export const NetworkStatus = ({
const [isConnecting, setIsConnecting] = React.useState<boolean>(false);
React.useEffect(() => {
let timeout: NodeJS.Timeout;
let timeout: any;
if (isConnecting) {
timeout = setTimeout(() => {

View file

@ -14,32 +14,15 @@ import { storiesOf } from '@storybook/react';
//import { boolean, select } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
// @ts-ignore
import gif from '../../fixtures/giphy-GVNvOUpeYmI7e.gif';
// @ts-ignore
import png from '../../fixtures/freepngs-2cd43b_bed7d1327e88454487397574d87b64dc_mv2.png';
// @ts-ignore
import landscapeGreen from '../../fixtures/1000x50-green.jpeg';
// @ts-ignore
import landscapePurple from '../../fixtures/200x50-purple.png';
import {
gifObjectUrl,
landscapeGreenObjectUrl,
landscapePurpleObjectUrl,
pngObjectUrl,
} from '../storybook/Fixtures';
const i18n = setupI18n('en', enMessages);
function makeObjectUrl(data: ArrayBuffer, contentType: string): string {
const blob = new Blob([data], {
type: contentType,
});
return URL.createObjectURL(blob);
}
// 320x240
const gifObjectUrl = makeObjectUrl(gif, 'image/gif');
// 800×1200
const pngObjectUrl = makeObjectUrl(png, 'image/png');
const landscapeGreenObjectUrl = makeObjectUrl(landscapeGreen, 'image/jpeg');
const landscapePurpleObjectUrl = makeObjectUrl(landscapePurple, 'image/png');
const messageLookup: Map<string, MessageSearchResultPropsType> = new Map();
const CONTACT = 'contact' as 'contact';
@ -64,6 +47,7 @@ messageLookup.set('1-guid-guid-guid-guid-guid', {
from: {
phoneNumber: '(202) 555-0020',
isMe: true,
color: 'blue',
avatarPath: gifObjectUrl,
},
to: {
@ -116,6 +100,7 @@ messageLookup.set('4-guid-guid-guid-guid-guid', {
from: {
phoneNumber: '(202) 555-0020',
isMe: true,
color: 'light_green',
avatarPath: gifObjectUrl,
},
to: {
@ -160,6 +145,7 @@ const conversations = [
phoneNumber: '(202) 555-0011',
name: 'Everyone 🌆',
type: GROUP,
color: 'signal-blue' as 'signal-blue',
avatarPath: landscapeGreenObjectUrl,
isMe: false,
lastUpdated: Date.now() - 5 * 60 * 1000,
@ -177,6 +163,7 @@ const conversations = [
id: '+12025550012',
phoneNumber: '(202) 555-0012',
name: 'Everyone Else 🔥',
color: 'pink' as 'pink',
type: DIRECT,
avatarPath: landscapePurpleObjectUrl,
isMe: false,
@ -198,6 +185,7 @@ const contacts = [
id: '+12025550013',
phoneNumber: '(202) 555-0013',
name: 'The one Everyone',
color: 'blue' as 'blue',
type: DIRECT,
avatarPath: gifObjectUrl,
isMe: false,
@ -213,7 +201,7 @@ const contacts = [
phoneNumber: '(202) 555-0014',
name: 'No likey everyone',
type: DIRECT,
color: 'red',
color: 'red' as 'red',
isMe: false,
lastUpdated: Date.now() - 11 * 60 * 1000,
unreadCount: 0,

View file

@ -1,156 +0,0 @@
### Name variations, 1:1 conversation
Note the five items in menu, and the second-level menu with disappearing messages options. Disappearing message set to 'off'.
#### With name and profile, verified
```jsx
<util.ConversationContext theme={util.theme}>
<ConversationHeader
i18n={util.i18n}
color="red"
isVerified={true}
avatarPath={util.gifObjectUrl}
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0001"
id="1"
profileName="🔥Flames🔥"
onSetDisappearingMessages={seconds =>
console.log('onSetDisappearingMessages', seconds)
}
onDeleteMessages={() => console.log('onDeleteMessages')}
onResetSession={() => console.log('onResetSession')}
onShowSafetyNumber={() => console.log('onShowSafetyNumber')}
onShowAllMedia={() => console.log('onShowAllMedia')}
onShowGroupMembers={() => console.log('onShowGroupMembers')}
onGoBack={() => console.log('onGoBack')}
onSearchInConversation={() => console.log('onSearchInConversation')}
/>
</util.ConversationContext>
```
#### With name, not verified, no avatar
```jsx
<util.ConversationContext theme={util.theme}>
<ConversationHeader
i18n={util.i18n}
color="blue"
isVerified={false}
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0002"
id="2"
/>
</util.ConversationContext>
```
#### Profile, no name
```jsx
<util.ConversationContext theme={util.theme}>
<ConversationHeader
i18n={util.i18n}
color="teal"
isVerified={false}
phoneNumber="(202) 555-0003"
id="3"
profileName="🔥Flames🔥"
/>
</util.ConversationContext>
```
#### No name, no profile, no color
```jsx
<util.ConversationContext theme={util.theme}>
<ConversationHeader i18n={util.i18n} phoneNumber="(202) 555-0011" id="11" />
</util.ConversationContext>
```
### With back button
```jsx
<util.ConversationContext theme={util.theme}>
<ConversationHeader
showBackButton={true}
color="deep_orange"
i18n={util.i18n}
phoneNumber="(202) 555-0004"
id="4"
/>
</util.ConversationContext>
```
### Disappearing messages set
```jsx
<util.ConversationContext theme={util.theme}>
<ConversationHeader
color="indigo"
i18n={util.i18n}
phoneNumber="(202) 555-0005"
id="5"
expirationSettingName="10 seconds"
timerOptions={[
{
name: 'off',
value: 0,
},
{
name: '10 seconds',
value: 10,
},
]}
onSetDisappearingMessages={seconds =>
console.log('onSetDisappearingMessages', seconds)
}
onDeleteMessages={() => console.log('onDeleteMessages')}
onResetSession={() => console.log('onResetSession')}
onShowSafetyNumber={() => console.log('onShowSafetyNumber')}
onShowAllMedia={() => console.log('onShowAllMedia')}
onShowGroupMembers={() => console.log('onShowGroupMembers')}
onGoBack={() => console.log('onGoBack')}
/>
</util.ConversationContext>
```
### In a group
Note that the menu should includes 'Show Members' instead of 'Show Safety Number'
```jsx
<util.ConversationContext theme={util.theme}>
<ConversationHeader
i18n={util.i18n}
color="green"
phoneNumber="(202) 555-0006"
id="6"
isGroup={true}
onSetDisappearingMessages={seconds =>
console.log('onSetDisappearingMessages', seconds)
}
onDeleteMessages={() => console.log('onDeleteMessages')}
onResetSession={() => console.log('onResetSession')}
onShowSafetyNumber={() => console.log('onShowSafetyNumber')}
onShowAllMedia={() => console.log('onShowAllMedia')}
onShowGroupMembers={() => console.log('onShowGroupMembers')}
onGoBack={() => console.log('onGoBack')}
/>
</util.ConversationContext>
```
### In chat with yourself
This is the 'Note to self' conversation. Note that the menu should not have a 'Show Safety Number' entry.
```jsx
<util.ConversationContext theme={util.theme}>
<ConversationHeader
color="cyan"
i18n={util.i18n}
phoneNumber="(202) 555-0007"
id="7"
isMe={true}
/>
</util.ConversationContext>
```

View file

@ -0,0 +1,227 @@
import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../\_locales/en/messages.json';
import {
ConversationHeader,
Props,
PropsActions,
PropsHousekeeping,
} from './ConversationHeader';
import { gifObjectUrl } from '../../storybook/Fixtures';
const book = storiesOf('Components/Conversation/ConversationHeader', module);
const i18n = setupI18n('en', enMessages);
type ConversationHeaderStory = {
title: string;
description: string;
items: Array<{
title: string;
props: Props;
}>;
};
const actionProps: PropsActions = {
onSetDisappearingMessages: action('onSetDisappearingMessages'),
onDeleteMessages: action('onDeleteMessages'),
onResetSession: action('onResetSession'),
onSearchInConversation: action('onSearchInConversation'),
onShowSafetyNumber: action('onShowSafetyNumber'),
onShowAllMedia: action('onShowAllMedia'),
onShowGroupMembers: action('onShowGroupMembers'),
onGoBack: action('onGoBack'),
onArchive: action('onArchive'),
onMoveToInbox: action('onMoveToInbox'),
};
const housekeepingProps: PropsHousekeeping = {
i18n,
};
const stories: Array<ConversationHeaderStory> = [
{
title: '1:1 conversation',
description:
"Note the five items in menu, and the second-level menu with disappearing messages options. Disappearing message set to 'off'.",
items: [
{
title: 'With name and profile, verified',
props: {
color: 'red',
isVerified: true,
avatarPath: gifObjectUrl,
name: 'Someone 🔥 Somewhere',
phoneNumber: '(202) 555-0001',
id: '1',
profileName: '🔥Flames🔥',
...actionProps,
...housekeepingProps,
},
},
{
title: 'With name, not verified, no avatar',
props: {
color: 'blue',
isVerified: false,
name: 'Someone 🔥 Somewhere',
phoneNumber: '(202) 555-0002',
id: '2',
...actionProps,
...housekeepingProps,
},
},
{
title: 'Profile, no name',
props: {
color: 'teal',
isVerified: false,
phoneNumber: '(202) 555-0003',
id: '3',
profileName: '🔥Flames🔥',
...actionProps,
...housekeepingProps,
},
},
{
title: 'No name, no profile, no color',
props: {
phoneNumber: '(202) 555-0011',
id: '11',
...actionProps,
...housekeepingProps,
},
},
{
title: 'With back button',
props: {
showBackButton: true,
color: 'deep_orange',
phoneNumber: '(202) 555-0004',
id: '4',
...actionProps,
...housekeepingProps,
},
},
{
title: 'Disappearing messages set',
props: {
color: 'indigo',
phoneNumber: '(202) 555-0005',
id: '5',
expirationSettingName: '10 seconds',
timerOptions: [
{
name: 'off',
value: 0,
},
{
name: '10 seconds',
value: 10,
},
],
...actionProps,
...housekeepingProps,
},
},
],
},
{
title: 'In a group',
description:
"Note that the menu should includes 'Show Members' instead of 'Show Safety Number'",
items: [
{
title: 'Basic',
props: {
color: 'signal-blue',
name: 'Typescript support group',
phoneNumber: '',
id: '1',
isGroup: true,
expirationSettingName: '10 seconds',
timerOptions: [
{
name: 'off',
value: 0,
},
{
name: '10 seconds',
value: 10,
},
],
...actionProps,
...housekeepingProps,
},
},
{
title: 'In a group you left - no disappearing messages',
props: {
color: 'signal-blue',
name: 'Typescript support group',
phoneNumber: '',
id: '2',
isGroup: true,
leftGroup: true,
expirationSettingName: '10 seconds',
timerOptions: [
{
name: 'off',
value: 0,
},
{
name: '10 seconds',
value: 10,
},
],
...actionProps,
...housekeepingProps,
},
},
],
},
{
title: 'Note to Self',
description: 'No safety number entry.',
items: [
{
title: 'In chat with yourself',
props: {
color: 'blue',
phoneNumber: '(202) 555-0007',
id: '7',
isMe: true,
...actionProps,
...housekeepingProps,
},
},
],
},
];
stories.forEach(({ title, description, items }) =>
book.add(
title,
() =>
items.map(({ title: subtitle, props }, i) => {
return (
<div key={i}>
{subtitle ? <h3>{subtitle}</h3> : null}
<ConversationHeader {...props} />
</div>
);
}),
{
docs: description,
}
)
);

View file

@ -3,7 +3,7 @@ import classNames from 'classnames';
import { Emojify } from './Emojify';
import { Avatar } from '../Avatar';
import { LocalizerType } from '../../types/Util';
import { ColorType, LocalizerType } from '../../types/Util';
import {
ContextMenu,
ContextMenuTrigger,
@ -16,24 +16,27 @@ interface TimerOption {
value: number;
}
interface Props {
export interface PropsData {
id: string;
name?: string;
phoneNumber: string;
profileName?: string;
color: string;
color?: ColorType;
avatarPath?: string;
isVerified: boolean;
isMe: boolean;
isGroup: boolean;
isArchived: boolean;
isVerified?: boolean;
isMe?: boolean;
isGroup?: boolean;
isArchived?: boolean;
leftGroup?: boolean;
expirationSettingName?: string;
showBackButton: boolean;
timerOptions: Array<TimerOption>;
showBackButton?: boolean;
timerOptions?: Array<TimerOption>;
}
export interface PropsActions {
onSetDisappearingMessages: (seconds: number) => void;
onDeleteMessages: () => void;
onResetSession: () => void;
@ -46,10 +49,14 @@ interface Props {
onArchive: () => void;
onMoveToInbox: () => void;
}
export interface PropsHousekeeping {
i18n: LocalizerType;
}
export type Props = PropsData & PropsActions & PropsHousekeeping;
export class ConversationHeader extends React.Component<Props> {
public showMenuBound: (event: React.MouseEvent<HTMLButtonElement>) => void;
public menuTriggerRef: React.RefObject<any>;
@ -218,6 +225,7 @@ export class ConversationHeader extends React.Component<Props> {
isMe,
isGroup,
isArchived,
leftGroup,
onDeleteMessages,
onResetSession,
onSetDisappearingMessages,
@ -233,18 +241,20 @@ export class ConversationHeader extends React.Component<Props> {
return (
<ContextMenu id={triggerId}>
<SubMenu title={disappearingTitle}>
{(timerOptions || []).map(item => (
<MenuItem
key={item.value}
onClick={() => {
onSetDisappearingMessages(item.value);
}}
>
{item.name}
</MenuItem>
))}
</SubMenu>
{leftGroup ? null : (
<SubMenu title={disappearingTitle}>
{(timerOptions || []).map(item => (
<MenuItem
key={item.value}
onClick={() => {
onSetDisappearingMessages(item.value);
}}
>
{item.name}
</MenuItem>
))}
</SubMenu>
)}
<MenuItem onClick={onShowAllMedia}>{i18n('viewAllMedia')}</MenuItem>
{isGroup ? (
<MenuItem onClick={onShowGroupMembers}>

View file

@ -1,165 +0,0 @@
### Three changes, all types
```js
<util.ConversationContext theme={util.theme}>
<GroupNotification
changes={[
{
type: 'add',
contacts: [
{
phoneNumber: '(202) 555-1000',
},
{
phoneNumber: '(202) 555-1001',
profileName: 'Mrs. Ice',
},
{
phoneNumber: '(202) 555-1002',
name: 'Ms. Earth',
},
],
},
{
type: 'name',
newName: 'New Group Name',
},
{
type: 'remove',
contacts: [
{
phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire',
},
],
},
]}
i18n={util.i18n}
/>
</util.ConversationContext>
```
### Joined group
```js
<util.ConversationContext theme={util.theme}>
<GroupNotification
changes={[
{
type: 'add',
contacts: [
{
phoneNumber: '(202) 555-1000',
},
{
phoneNumber: '(202) 555-1001',
profileName: 'Mrs. Ice',
},
{
phoneNumber: '(202) 555-1002',
name: 'Ms. Earth',
},
],
},
]}
i18n={util.i18n}
/>
<GroupNotification
changes={[
{
type: 'add',
contacts: [
{
phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire',
},
],
},
]}
i18n={util.i18n}
/>
</util.ConversationContext>
```
### Left group
```js
<util.ConversationContext theme={util.theme}>
<GroupNotification
changes={[
{
type: 'remove',
contacts: [
{
phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire',
},
{
phoneNumber: '(202) 555-1001',
profileName: 'Mrs. Ice',
},
{
phoneNumber: '(202) 555-1002',
name: 'Ms. Earth',
},
],
},
]}
i18n={util.i18n}
/>
<GroupNotification
changes={[
{
type: 'remove',
contacts: [
{
phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire',
},
],
},
]}
i18n={util.i18n}
/>
<GroupNotification
changes={[
{
type: 'remove',
isMe: true,
},
]}
i18n={util.i18n}
/>
</util.ConversationContext>
```
### Title changed
```js
<util.ConversationContext theme={util.theme}>
<GroupNotification
changes={[
{
type: 'name',
newName: 'New Group Name',
},
]}
i18n={util.i18n}
/>
</util.ConversationContext>
```
### Generic group update
```js
<util.ConversationContext theme={util.theme}>
<GroupNotification
changes={[
{
type: 'general',
},
]}
i18n={util.i18n}
/>
</util.ConversationContext>
```

View file

@ -0,0 +1,353 @@
import * as React from 'react';
import { storiesOf } from '@storybook/react';
// @ts-ignore
import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../../_locales/en/messages.json';
import { GroupNotification, Props } from './GroupNotification';
const book = storiesOf('Components/Conversation', module);
const i18n = setupI18n('en', enMessages);
type GroupNotificationStory = [string, Array<Props>];
const stories: Array<GroupNotificationStory> = [
[
'Combo',
[
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
},
changes: [
{
type: 'add',
contacts: [
{
phoneNumber: '(202) 555-1001',
profileName: 'Mrs. Ice',
},
{
phoneNumber: '(202) 555-1002',
name: 'Ms. Earth',
},
],
},
{ type: 'name', newName: 'Fishing Stories' },
{ type: 'avatar' },
],
i18n,
},
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
isMe: true,
},
changes: [
{
type: 'add',
contacts: [
{
phoneNumber: '(202) 555-1001',
profileName: 'Mrs. Ice',
},
{
phoneNumber: '(202) 555-1002',
name: 'Ms. Earth',
},
],
},
{ type: 'name', newName: 'Fishing Stories' },
{ type: 'avatar' },
],
i18n,
},
],
],
[
'Joined group',
[
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
},
changes: [
{
type: 'add',
contacts: [
{
phoneNumber: '(202) 555-1000',
},
{
phoneNumber: '(202) 555-1001',
profileName: 'Mrs. Ice',
},
{
phoneNumber: '(202) 555-1002',
name: 'Ms. Earth',
},
],
},
],
i18n,
},
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
},
changes: [
{
type: 'add',
contacts: [
{
phoneNumber: '(202) 555-1000',
isMe: true,
},
{
phoneNumber: '(202) 555-1001',
profileName: 'Mrs. Ice',
},
{
phoneNumber: '(202) 555-1002',
name: 'Ms. Earth',
},
],
},
],
i18n,
},
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
},
changes: [
{
type: 'add',
contacts: [
{
phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire',
},
],
},
],
i18n,
},
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
isMe: true,
},
changes: [
{
type: 'add',
contacts: [
{
phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire',
},
],
},
],
i18n,
},
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
},
changes: [
{
type: 'add',
contacts: [
{
phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire',
isMe: true,
},
],
},
],
i18n,
},
],
],
[
'Left group',
[
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
},
changes: [
{
type: 'remove',
contacts: [
{
phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire',
},
{
phoneNumber: '(202) 555-1001',
profileName: 'Mrs. Ice',
},
{
phoneNumber: '(202) 555-1002',
name: 'Ms. Earth',
},
],
},
],
i18n,
},
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
},
changes: [
{
type: 'remove',
contacts: [
{
phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire',
},
],
},
],
i18n,
},
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
isMe: true,
},
changes: [
{
type: 'remove',
contacts: [
{
name: 'Alice',
phoneNumber: '(202) 555-1000',
isMe: true,
},
],
},
],
i18n,
},
],
],
[
'Title changed',
[
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
},
changes: [
{
type: 'name',
newName: 'New Group Name',
},
],
i18n,
},
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
isMe: true,
},
changes: [
{
type: 'name',
newName: 'New Group Name',
},
],
i18n,
},
],
],
[
'Avatar changed',
[
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
},
changes: [
{
type: 'avatar',
newName: 'New Group Name',
},
],
i18n,
},
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
isMe: true,
},
changes: [
{
type: 'avatar',
newName: 'New Group Name',
},
],
i18n,
},
],
],
[
'Generic group update',
[
{
from: {
name: 'Alice',
phoneNumber: '(202) 555-1000',
},
changes: [
{
type: 'general',
},
],
i18n,
},
],
],
];
book.add('GroupNotification', () =>
stories.map(([title, propsArray]) => (
<>
<h3>{title}</h3>
{propsArray.map((props, i) => {
return (
<>
<div key={i} className="module-message-container">
<div className="module-inline-notification-wrapper">
<GroupNotification {...props} />
</div>
</div>
</>
);
})}
</>
))
);

View file

@ -1,10 +1,8 @@
import React from 'react';
// import classNames from 'classnames';
import { compact, flatten } from 'lodash';
import { ContactName } from './ContactName';
import { Emojify } from './Emojify';
import { Intl } from '../Intl';
import { FullJSXType, Intl } from '../Intl';
import { LocalizerType } from '../../types/Util';
import { missingCaseError } from '../../util/missingCaseError';
@ -13,16 +11,17 @@ interface Contact {
phoneNumber: string;
profileName?: string;
name?: string;
isMe?: boolean;
}
interface Change {
type: 'add' | 'remove' | 'name' | 'general';
isMe: boolean;
type: 'add' | 'remove' | 'name' | 'avatar' | 'general';
newName?: string;
contacts?: Array<Contact>;
}
export type PropsData = {
from: Contact;
changes: Array<Change>;
};
@ -30,48 +29,82 @@ type PropsHousekeeping = {
i18n: LocalizerType;
};
type Props = PropsData & PropsHousekeeping;
export type Props = PropsData & PropsHousekeeping;
export class GroupNotification extends React.Component<Props> {
public renderChange(change: Change) {
const { isMe, contacts, type, newName } = change;
public renderChange(change: Change, from: Contact) {
const { contacts, type, newName } = change;
const { i18n } = this.props;
const people = compact(
flatten(
(contacts || []).map((contact, index) => {
const element = (
<span
key={`external-${contact.phoneNumber}`}
className="module-group-notification__contact"
>
<ContactName
phoneNumber={contact.phoneNumber}
profileName={contact.profileName}
name={contact.name}
/>
</span>
);
const otherPeople = compact(
(contacts || []).map(contact => {
if (contact.isMe) {
return null;
}
return [index > 0 ? ', ' : null, element];
})
return (
<span
key={`external-${contact.phoneNumber}`}
className="module-group-notification__contact"
>
<ContactName
phoneNumber={contact.phoneNumber}
profileName={contact.profileName}
name={contact.name}
/>
</span>
);
})
);
const otherPeopleWithCommas: FullJSXType = compact(
flatten(
otherPeople.map((person, index) => [index > 0 ? ', ' : null, person])
)
);
const contactsIncludesMe = (contacts || []).length !== otherPeople.length;
switch (type) {
case 'name':
return <Emojify text={i18n('titleIsNow', [newName || ''])} />;
return (
<Intl i18n={i18n} id="titleIsNow" components={[newName || '']} />
);
case 'avatar':
return <Intl i18n={i18n} id="updatedGroupAvatar" />;
case 'add':
if (!contacts || !contacts.length) {
throw new Error('Group update is missing contacts');
}
const joinKey =
contacts.length > 1 ? 'multipleJoinedTheGroup' : 'joinedTheGroup';
if (contacts.length === 1) {
if (contactsIncludesMe) {
return <Intl i18n={i18n} id="youJoinedTheGroup" />;
} else {
return (
<Intl
i18n={i18n}
id="joinedTheGroup"
components={[otherPeopleWithCommas]}
/>
);
}
}
return <Intl i18n={i18n} id={joinKey} components={[people]} />;
return (
<>
<Intl
i18n={i18n}
id="multipleJoinedTheGroup"
components={[otherPeopleWithCommas]}
/>
{contactsIncludesMe ? (
<div className="module-group-notification__change">
<Intl i18n={i18n} id="youJoinedTheGroup" />
</div>
) : null}
</>
);
case 'remove':
if (isMe) {
if (from && from.isMe) {
return i18n('youLeftTheGroup');
}
@ -82,22 +115,49 @@ export class GroupNotification extends React.Component<Props> {
const leftKey =
contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup';
return <Intl i18n={i18n} id={leftKey} components={[people]} />;
return (
<Intl i18n={i18n} id={leftKey} components={[otherPeopleWithCommas]} />
);
case 'general':
return i18n('updatedTheGroup');
return;
default:
throw missingCaseError(type);
}
}
public render() {
const { changes } = this.props;
const { changes, i18n, from } = this.props;
// Leave messages are always from the person leaving, so we omit the fromLabel if
// the change is a 'leave.'
const isLeftOnly =
changes && changes.length === 1 && changes[0].type === 'remove';
const fromContact = (
<ContactName
phoneNumber={from.phoneNumber}
profileName={from.profileName}
name={from.name}
/>
);
const fromLabel = from.isMe ? (
<Intl i18n={i18n} id="youUpdatedTheGroup" />
) : (
<Intl i18n={i18n} id="updatedTheGroup" components={[fromContact]} />
);
return (
<div className="module-group-notification">
{isLeftOnly ? null : (
<>
{fromLabel}
<br />
</>
)}
{(changes || []).map((change, index) => (
<div key={index} className="module-group-notification__change">
{this.renderChange(change)}
{this.renderChange(change, from)}
</div>
))}
</div>

View file

@ -106,7 +106,7 @@ const stories: Array<MessageStory> = [
makeDataProps: () => ({
...baseDataProps,
direction: 'incoming',
authorColor: 'gray',
authorColor: 'grey',
text:
'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
}),

View file

@ -5,7 +5,7 @@ import moment from 'moment';
import { Avatar } from '../Avatar';
import { ContactName } from './ContactName';
import { Message, Props as MessageProps } from './Message';
import { LocalizerType } from '../../types/Util';
import { ColorType, LocalizerType } from '../../types/Util';
interface Contact {
status: string;
@ -13,7 +13,7 @@ interface Contact {
name?: string;
profileName?: string;
avatarPath?: string;
color: string;
color: ColorType;
isOutgoingKeyError: boolean;
isUnidentifiedDelivery: boolean;

View file

@ -4,11 +4,11 @@ import classNames from 'classnames';
import { TypingAnimation } from './TypingAnimation';
import { Avatar } from '../Avatar';
import { LocalizerType } from '../../types/Util';
import { ColorType, LocalizerType } from '../../types/Util';
interface Props {
avatarPath?: string;
color: string;
color: ColorType;
name?: string;
phoneNumber: string;
profileName?: string;