Include sender in group update notifications
This commit is contained in:
parent
d88c21e5b6
commit
71436d18e2
28 changed files with 1016 additions and 472 deletions
|
@ -268,7 +268,7 @@
|
|||
"description": "Used as a label on a button allowing user to see more information"
|
||||
},
|
||||
"youLeftTheGroup": {
|
||||
"message": "You left the group",
|
||||
"message": "You left the group.",
|
||||
"description": "Displayed when a user can't send a message because they have left the group"
|
||||
},
|
||||
"scrollDown": {
|
||||
|
@ -1591,7 +1591,7 @@
|
|||
"message": "Later"
|
||||
},
|
||||
"leftTheGroup": {
|
||||
"message": "$name$ left the group",
|
||||
"message": "$name$ left the group.",
|
||||
"description": "Shown in the conversation history when a single person leaves the group",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
|
@ -1601,7 +1601,7 @@
|
|||
}
|
||||
},
|
||||
"multipleLeftTheGroup": {
|
||||
"message": "$name$ left the group",
|
||||
"message": "$name$ left the group.",
|
||||
"description": "Shown in the conversation history when multiple people leave the group",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
|
@ -1611,11 +1611,25 @@
|
|||
}
|
||||
},
|
||||
"updatedTheGroup": {
|
||||
"message": "Group updated",
|
||||
"message": "$name$ updated the group.",
|
||||
"description": "Shown in the conversation history when someone updates the group",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
"example": "Alice"
|
||||
}
|
||||
}
|
||||
},
|
||||
"youUpdatedTheGroup": {
|
||||
"message": "You updated the group.",
|
||||
"description": "Shown in the conversation history when you update a group"
|
||||
},
|
||||
"updatedGroupAvatar": {
|
||||
"message": "Group avatar was updated.",
|
||||
"description": "Shown in the conversation history when someone updates the group"
|
||||
},
|
||||
"titleIsNow": {
|
||||
"message": "Title is now '$name$'",
|
||||
"message": "Group name is now '$name$'.",
|
||||
"description": "Shown in the conversation history when someone changes the title of the group",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
|
@ -1624,8 +1638,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"youJoinedTheGroup": {
|
||||
"message": "You joined the group.",
|
||||
"description": "Shown in the conversation history when you are added to a group."
|
||||
},
|
||||
"joinedTheGroup": {
|
||||
"message": "$name$ joined the group",
|
||||
"message": "$name$ joined the group.",
|
||||
"description": "Shown in the conversation history when a single person joins the group",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
|
@ -1635,7 +1653,7 @@
|
|||
}
|
||||
},
|
||||
"multipleJoinedTheGroup": {
|
||||
"message": "$names$ joined the group",
|
||||
"message": "$names$ joined the group.",
|
||||
"description": "Shown in the conversation history when more than one person joins the group",
|
||||
"placeholders": {
|
||||
"names": {
|
||||
|
|
|
@ -1549,6 +1549,9 @@
|
|||
) {
|
||||
let expireTimer = providedExpireTimer;
|
||||
let source = providedSource;
|
||||
if (this.get('left')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_.defaults(options, { fromSync: false, fromGroupUpdate: false });
|
||||
|
||||
|
|
|
@ -432,7 +432,12 @@
|
|||
const groupUpdate = this.get('group_update');
|
||||
const changes = [];
|
||||
|
||||
if (!groupUpdate.name && !groupUpdate.left && !groupUpdate.joined) {
|
||||
if (
|
||||
!groupUpdate.avatarUpdated &&
|
||||
!groupUpdate.left &&
|
||||
!groupUpdate.joined &&
|
||||
!groupUpdate.name
|
||||
) {
|
||||
changes.push({
|
||||
type: 'general',
|
||||
});
|
||||
|
@ -474,7 +479,18 @@
|
|||
});
|
||||
}
|
||||
|
||||
if (groupUpdate.avatarUpdated) {
|
||||
changes.push({
|
||||
type: 'avatar',
|
||||
});
|
||||
}
|
||||
|
||||
const sourceE164 = this.getSource();
|
||||
const sourceUuid = this.getSourceUuid();
|
||||
const from = this.findAndFormatContact(sourceE164 || sourceUuid);
|
||||
|
||||
return {
|
||||
from,
|
||||
changes,
|
||||
};
|
||||
},
|
||||
|
@ -834,34 +850,72 @@
|
|||
|
||||
return i18n('mediaMessage');
|
||||
}
|
||||
|
||||
if (this.isGroupUpdate()) {
|
||||
const groupUpdate = this.get('group_update');
|
||||
const fromContact = this.getContact();
|
||||
const messages = [];
|
||||
|
||||
if (groupUpdate.left === 'You') {
|
||||
return i18n('youLeftTheGroup');
|
||||
} else if (groupUpdate.left) {
|
||||
return i18n('leftTheGroup', this.getNameForNumber(groupUpdate.left));
|
||||
}
|
||||
|
||||
const messages = [];
|
||||
if (!groupUpdate.name && !groupUpdate.joined) {
|
||||
messages.push(i18n('updatedTheGroup'));
|
||||
if (!fromContact) {
|
||||
return '';
|
||||
}
|
||||
if (groupUpdate.name) {
|
||||
messages.push(i18n('titleIsNow', groupUpdate.name));
|
||||
|
||||
if (fromContact.isMe()) {
|
||||
messages.push(i18n('youUpdatedTheGroup'));
|
||||
} else {
|
||||
messages.push(i18n('updatedTheGroup', fromContact.getDisplayName()));
|
||||
}
|
||||
|
||||
if (groupUpdate.joined && groupUpdate.joined.length) {
|
||||
const names = _.map(
|
||||
groupUpdate.joined,
|
||||
this.getNameForNumber.bind(this)
|
||||
const joinedContacts = _.map(groupUpdate.joined, item =>
|
||||
ConversationController.getOrCreate(item, 'private')
|
||||
);
|
||||
if (names.length > 1) {
|
||||
messages.push(i18n('multipleJoinedTheGroup', names.join(', ')));
|
||||
const joinedWithoutMe = joinedContacts.filter(
|
||||
contact => !contact.isMe()
|
||||
);
|
||||
|
||||
if (joinedContacts.length > 1) {
|
||||
messages.push(
|
||||
i18n(
|
||||
'multipleJoinedTheGroup',
|
||||
_.map(joinedWithoutMe, contact =>
|
||||
contact.getDisplayName()
|
||||
).join(', ')
|
||||
)
|
||||
);
|
||||
|
||||
if (joinedWithoutMe.length < joinedContacts.length) {
|
||||
messages.push(i18n('youJoinedTheGroup'));
|
||||
}
|
||||
} else {
|
||||
messages.push(i18n('joinedTheGroup', names[0]));
|
||||
const joinedContact = ConversationController.getOrCreate(
|
||||
groupUpdate.joined[0],
|
||||
'private'
|
||||
);
|
||||
if (joinedContact.isMe()) {
|
||||
messages.push(i18n('youJoinedTheGroup'));
|
||||
} else {
|
||||
messages.push(
|
||||
i18n('joinedTheGroup', joinedContacts[0].getDisplayName())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messages.join(', ');
|
||||
if (groupUpdate.name) {
|
||||
messages.push(i18n('titleIsNow', groupUpdate.name));
|
||||
}
|
||||
if (groupUpdate.avatarUpdated) {
|
||||
messages.push(i18n('updatedGroupAvatar'));
|
||||
}
|
||||
|
||||
return messages.join(' ');
|
||||
}
|
||||
if (this.isEndSession()) {
|
||||
return i18n('sessionEnded');
|
||||
|
@ -2165,10 +2219,13 @@
|
|||
members: _.union(members, conversation.get('members')),
|
||||
};
|
||||
|
||||
groupUpdate =
|
||||
conversation.changedAttributes(
|
||||
_.pick(dataMessage.group, 'name', 'avatar')
|
||||
) || {};
|
||||
groupUpdate = {};
|
||||
if (dataMessage.group.name !== conversation.get('name')) {
|
||||
groupUpdate.name = dataMessage.group.name;
|
||||
}
|
||||
|
||||
// Note: used and later cleared by background attachment downloader
|
||||
groupUpdate.avatar = dataMessage.group.avatar;
|
||||
|
||||
const difference = _.difference(
|
||||
members,
|
||||
|
|
|
@ -422,21 +422,39 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) {
|
|||
return;
|
||||
}
|
||||
|
||||
const existingAvatar = conversation.get('avatar');
|
||||
if (existingAvatar && existingAvatar.path) {
|
||||
await Signal.Migrations.deleteAttachmentData(existingAvatar.path);
|
||||
}
|
||||
|
||||
const loadedAttachment = await Signal.Migrations.loadAttachmentData(
|
||||
attachment
|
||||
);
|
||||
const hash = await computeHash(loadedAttachment.data);
|
||||
const existingAvatar = conversation.get('avatar');
|
||||
|
||||
if (existingAvatar) {
|
||||
if (existingAvatar.hash === hash) {
|
||||
logger.info(
|
||||
'_addAttachmentToMessage: Group avatar hash matched; not replacing group avatar'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await Signal.Migrations.deleteAttachmentData(existingAvatar.path);
|
||||
}
|
||||
|
||||
conversation.set({
|
||||
avatar: {
|
||||
...attachment,
|
||||
hash: await computeHash(loadedAttachment.data),
|
||||
hash,
|
||||
},
|
||||
});
|
||||
Signal.Data.updateConversation(conversationId, conversation.attributes);
|
||||
|
||||
message.set({
|
||||
group_update: {
|
||||
...message.get('group_update'),
|
||||
avatar: null,
|
||||
avatarUpdated: true,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -371,6 +371,7 @@
|
|||
isMe: this.model.isMe(),
|
||||
isGroup: !this.model.isPrivate(),
|
||||
isArchived: this.model.get('isArchived'),
|
||||
leftGroup: this.model.get('left'),
|
||||
|
||||
expirationSettingName,
|
||||
showBackButton: Boolean(this.panels && this.panels.length),
|
||||
|
|
|
@ -2255,6 +2255,8 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
|||
.module-group-notification {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
|
@ -2267,8 +2269,8 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
|||
}
|
||||
|
||||
.module-group-notification__change {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.module-group-notification__contact {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global ConversationController, i18n, Whisper */
|
||||
/* global ConversationController, i18n, Whisper, textsecure */
|
||||
|
||||
'use strict';
|
||||
|
||||
|
@ -10,15 +10,21 @@ const attributes = {
|
|||
received_at: new Date().getTime(),
|
||||
};
|
||||
|
||||
const source = '+14155555555';
|
||||
const source = '+1 415-555-5555';
|
||||
const me = '+14155555556';
|
||||
const ourUuid = window.getGuid();
|
||||
|
||||
describe('MessageCollection', () => {
|
||||
before(async () => {
|
||||
await clearDatabase();
|
||||
ConversationController.reset();
|
||||
await ConversationController.load();
|
||||
textsecure.storage.put('number_id', `${me}.2`);
|
||||
textsecure.storage.put('uuid_id', `${ourUuid}.2`);
|
||||
});
|
||||
after(() => {
|
||||
textsecure.storage.put('number_id', null);
|
||||
textsecure.storage.put('uuid_id', null);
|
||||
return clearDatabase();
|
||||
});
|
||||
|
||||
|
@ -91,46 +97,128 @@ describe('MessageCollection', () => {
|
|||
'If no group updates or end session flags, return message body.'
|
||||
);
|
||||
|
||||
message = messages.add({ group_update: { left: 'Alice' } });
|
||||
message = messages.add({
|
||||
group_update: {},
|
||||
source: 'Alice',
|
||||
type: 'incoming',
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
'Alice left the group',
|
||||
'Alice updated the group.',
|
||||
'Empty group updates - generic message.'
|
||||
);
|
||||
|
||||
message = messages.add({
|
||||
type: 'incoming',
|
||||
source,
|
||||
group_update: { left: 'Alice' },
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
'Alice left the group.',
|
||||
'Notes one person leaving the group.'
|
||||
);
|
||||
|
||||
message = messages.add({ group_update: { name: 'blerg' } });
|
||||
message = messages.add({
|
||||
type: 'incoming',
|
||||
source: me,
|
||||
group_update: { left: 'You' },
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
"Title is now 'blerg'",
|
||||
'Returns a single notice if only group_updates.name changes.'
|
||||
'You left the group.',
|
||||
'Notes that you left the group.'
|
||||
);
|
||||
|
||||
message = messages.add({ group_update: { joined: ['Bob'] } });
|
||||
message = messages.add({
|
||||
type: 'incoming',
|
||||
source,
|
||||
group_update: { name: 'blerg' },
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
'Bob joined the group',
|
||||
"+1 415-555-5555 updated the group. Group name is now 'blerg'.",
|
||||
'Returns sender and name change.'
|
||||
);
|
||||
|
||||
message = messages.add({
|
||||
type: 'incoming',
|
||||
source: me,
|
||||
group_update: { name: 'blerg' },
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
"You updated the group. Group name is now 'blerg'.",
|
||||
'Includes "you" as sender along with group name change.'
|
||||
);
|
||||
|
||||
message = messages.add({
|
||||
type: 'incoming',
|
||||
source,
|
||||
group_update: { avatarUpdated: true },
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
'+1 415-555-5555 updated the group. Group avatar was updated.',
|
||||
'Includes sender and avatar update.'
|
||||
);
|
||||
|
||||
message = messages.add({
|
||||
type: 'incoming',
|
||||
source,
|
||||
group_update: { joined: [me] },
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
'+1 415-555-5555 updated the group. You joined the group.',
|
||||
'Includes both sender and person added with join.'
|
||||
);
|
||||
|
||||
message = messages.add({
|
||||
type: 'incoming',
|
||||
source,
|
||||
group_update: { joined: ['Bob'] },
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
'+1 415-555-5555 updated the group. Bob joined the group.',
|
||||
'Returns a single notice if only group_updates.joined changes.'
|
||||
);
|
||||
|
||||
message = messages.add({
|
||||
type: 'incoming',
|
||||
source,
|
||||
group_update: { joined: ['Bob', 'Alice', 'Eve'] },
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
'Bob, Alice, Eve joined the group',
|
||||
'+1 415-555-5555 updated the group. Bob, Alice, Eve joined the group.',
|
||||
'Notes when >1 person joins the group.'
|
||||
);
|
||||
|
||||
message = messages.add({
|
||||
type: 'incoming',
|
||||
source,
|
||||
group_update: { joined: ['Bob', me, 'Alice', 'Eve'] },
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
'+1 415-555-5555 updated the group. Bob, Alice, Eve joined the group. You joined the group.',
|
||||
'Splits "You" out when multiple people are added along with you.'
|
||||
);
|
||||
|
||||
message = messages.add({
|
||||
type: 'incoming',
|
||||
source,
|
||||
group_update: { joined: ['Bob'], name: 'blerg' },
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
"Title is now 'blerg', Bob joined the group",
|
||||
"+1 415-555-5555 updated the group. Bob joined the group. Group name is now 'blerg'.",
|
||||
'Notes when there are multiple changes to group_updates properties.'
|
||||
);
|
||||
|
||||
message = messages.add({ flags: true });
|
||||
message = messages.add({ type: 'incoming', source, flags: true });
|
||||
assert.equal(message.getDescription(), i18n('sessionEnded'));
|
||||
});
|
||||
|
||||
|
@ -139,7 +227,7 @@ describe('MessageCollection', () => {
|
|||
let message = messages.add(attributes);
|
||||
assert.notOk(message.isEndSession());
|
||||
|
||||
message = messages.add({ flags: true });
|
||||
message = messages.add({ type: 'incoming', source, flags: true });
|
||||
assert.ok(message.isEndSession());
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
```
|
227
ts/components/conversation/ConversationHeader.stories.tsx
Normal file
227
ts/components/conversation/ConversationHeader.stories.tsx
Normal 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,
|
||||
}
|
||||
)
|
||||
);
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
```
|
353
ts/components/conversation/GroupNotification.stories.tsx
Normal file
353
ts/components/conversation/GroupNotification.stories.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
))
|
||||
);
|
|
@ -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>
|
||||
|
|
|
@ -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.',
|
||||
}),
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -90,6 +90,7 @@ export type MessageType = {
|
|||
}>;
|
||||
|
||||
errors?: Array<Error>;
|
||||
group_update?: any;
|
||||
|
||||
// No need to go beyond this; unused at this stage, since this goes into
|
||||
// a reducer still in plain JavaScript and comes out well-formed
|
||||
|
@ -581,6 +582,11 @@ function hasMessageHeightChanged(
|
|||
return true;
|
||||
}
|
||||
|
||||
const groupUpdateChanged = message.group_update !== previous.group_update;
|
||||
if (groupUpdateChanged) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const stickerPendingChanged =
|
||||
message.sticker &&
|
||||
message.sticker.data &&
|
||||
|
|
30
ts/storybook/Fixtures.ts
Normal file
30
ts/storybook/Fixtures.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
// @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';
|
||||
|
||||
function makeObjectUrl(data: ArrayBuffer, contentType: string): string {
|
||||
const blob = new Blob([data], {
|
||||
type: contentType,
|
||||
});
|
||||
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
// 320x240
|
||||
export const gifObjectUrl = makeObjectUrl(gif, 'image/gif');
|
||||
|
||||
// 800×1200
|
||||
export const pngObjectUrl = makeObjectUrl(png, 'image/png');
|
||||
export const landscapeGreenObjectUrl = makeObjectUrl(
|
||||
landscapeGreen,
|
||||
'image/jpeg'
|
||||
);
|
||||
export const landscapePurpleObjectUrl = makeObjectUrl(
|
||||
landscapePurple,
|
||||
'image/png'
|
||||
);
|
|
@ -6,14 +6,17 @@ export type RenderTextCallbackType = (options: {
|
|||
export type LocalizerType = (key: string, values?: Array<string>) => string;
|
||||
|
||||
export type ColorType =
|
||||
| 'gray'
|
||||
| 'blue'
|
||||
| 'cyan'
|
||||
| 'red'
|
||||
| 'deep_orange'
|
||||
| 'green'
|
||||
| 'indigo'
|
||||
| 'brown'
|
||||
| 'pink'
|
||||
| 'purple'
|
||||
| 'red'
|
||||
| 'indigo'
|
||||
| 'blue'
|
||||
| 'teal'
|
||||
| 'ultramarine';
|
||||
| 'green'
|
||||
| 'light_green'
|
||||
| 'blue_grey'
|
||||
| 'grey'
|
||||
| 'ultramarine'
|
||||
| 'signal-blue';
|
||||
|
|
|
@ -11508,7 +11508,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/ConversationHeader.tsx",
|
||||
"line": " this.menuTriggerRef = React.createRef();",
|
||||
"lineNumber": 60,
|
||||
"lineNumber": 67,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Used to reference popup menu"
|
||||
|
|
Loading…
Reference in a new issue