Adding new group members; establishing sessions between non-friends

pull/565/head
Maxim Shishmarev 6 years ago
parent 0d19b708f9
commit cf18572049

@ -1910,6 +1910,13 @@
"description": "description":
"Button action that the user can click to copy their public keys" "Button action that the user can click to copy their public keys"
}, },
"updateGroup": {
"message": "Update Group",
"description":
"Button action that the user can click to rename the group or add a new member"
},
"copiedPublicKey": { "copiedPublicKey": {
"message": "Copied public key", "message": "Copied public key",
"description": "A toast message telling the user that the key was copied" "description": "A toast message telling the user that the key was copied"
@ -1935,6 +1942,12 @@
"description": "Title for the dialog box used to create a new private group" "description": "Title for the dialog box used to create a new private group"
}, },
"updateGroupDialogTitle": {
"message": "Updating a Private Group Chat",
"description":
"Title for the dialog box used to update an existing private group"
},
"showSeed": { "showSeed": {
"message": "Show seed", "message": "Show seed",
"description": "description":

@ -682,6 +682,38 @@
} }
}); });
window.doUpdateGroup = async (groupId, groupName, members) => {
const ourKey = textsecure.storage.user.getNumber();
const ev = new Event('message');
ev.confirm = () => {};
ev.data = {
source: ourKey,
message: {
group: {
id: groupId,
type: textsecure.protobuf.GroupContext.Type.UPDATE,
name: groupName,
members,
avatar: null, // TODO
},
},
};
await onMessageReceived(ev);
const avatar = '';
const options = {};
textsecure.messaging.updateGroup(
groupId,
groupName,
avatar,
members,
options
);
};
window.doCreateGroup = async (groupName, members) => { window.doCreateGroup = async (groupName, members) => {
const keypair = await libsignal.KeyHelper.generateIdentityKeyPair(); const keypair = await libsignal.KeyHelper.generateIdentityKeyPair();
const groupId = StringView.arrayBufferToHex(keypair.pubKey); const groupId = StringView.arrayBufferToHex(keypair.pubKey);
@ -696,6 +728,7 @@
members: [ourKey, ...members], members: [ourKey, ...members],
active: true, active: true,
expireTimer: 0, expireTimer: 0,
avatar: '',
}; };
ev.confirm = () => {}; ev.confirm = () => {};
@ -707,21 +740,15 @@
'group' 'group'
); );
convo.updateGroup(ev.groupDetails);
// Group conversations are automatically 'friends'
// so that we can skip the friend request logic
convo.setFriendRequestStatus( convo.setFriendRequestStatus(
window.friends.friendRequestStatusEnum.friends window.friends.friendRequestStatusEnum.friends
); );
convo.set({ active_at: Date.now() });
appView.openConversation(groupId, {}); appView.openConversation(groupId, {});
// Tell all group participants about this group
textsecure.messaging.createGroup(
ev.groupDetails.members,
groupId,
ev.groupDetails.name,
{},
{}
);
}; };
Whisper.events.on('createNewGroup', async () => { Whisper.events.on('createNewGroup', async () => {
@ -730,6 +757,12 @@
} }
}); });
Whisper.events.on('updateGroup', async groupConvo => {
if (appView) {
appView.showUpdateGroupDialog(groupConvo);
}
});
Whisper.events.on('deleteConversation', async conversation => { Whisper.events.on('deleteConversation', async conversation => {
await conversation.destroyMessages(); await conversation.destroyMessages();
await window.Signal.Data.removeConversation(conversation.id, { await window.Signal.Data.removeConversation(conversation.id, {

@ -1700,11 +1700,71 @@
const conversation = ConversationController.get(conversationId); const conversation = ConversationController.get(conversationId);
if (initialMessage.group) { if (
// TODO: call this only once! initialMessage.group &&
initialMessage.group.members &&
initialMessage.group.type === GROUP_TYPES.UPDATE
) {
// Note: this might be called more times than necessary
conversation.setFriendRequestStatus( conversation.setFriendRequestStatus(
window.friends.friendRequestStatusEnum.friends window.friends.friendRequestStatusEnum.friends
); );
// For every member, see if we need to establish a session:
initialMessage.group.members.forEach(memberPubKey => {
const haveSession = _.some(
textsecure.storage.protocol.sessions,
s => s.number === memberPubKey
);
const ourPubKey = textsecure.storage.user.getNumber();
if (!haveSession && memberPubKey !== ourPubKey) {
ConversationController.getOrCreateAndWait(
memberPubKey,
'private'
).then(() => {
textsecure.messaging.sendMessageToNumber(
memberPubKey,
'(If you see this message, you must be using an out-of-date client)',
[],
undefined,
[],
Date.now(),
undefined,
undefined,
{ messageType: 'friend-request', backgroundFriendReq: true }
);
});
}
});
}
const backgroundFrReq =
initialMessage.flags ===
textsecure.protobuf.DataMessage.Flags.BACKGROUND_FRIEND_REQUEST;
if (message.isFriendRequest() && backgroundFrReq) {
// Check if the contact is a member in one of our private groups:
const groupMember =
window
.getConversations()
.models.filter(c => c.get('members'))
.reduce((acc, x) => window.Lodash.concat(acc, x.get('members')), [])
.indexOf(source) !== -1;
if (groupMember) {
window.log.info(
`Auto accepting a 'group' friend request for a known group member: ${groupMember}`
);
window.libloki.api.sendBackgroundMessage(message.get('source'));
confirm();
}
// Wether or not we accepted the FR, we exit early so background friend requests
// cannot be used for establishing regular private conversations
return null;
} }
return conversation.queueJob(async () => { return conversation.queueJob(async () => {

@ -47,6 +47,9 @@ const { MemberList } = require('../../ts/components/conversation/MemberList');
const { const {
CreateGroupDialog, CreateGroupDialog,
} = require('../../ts/components/conversation/CreateGroupDialog'); } = require('../../ts/components/conversation/CreateGroupDialog');
const {
UpdateGroupDialog,
} = require('../../ts/components/conversation/UpdateGroupDialog');
const { const {
MediaGallery, MediaGallery,
} = require('../../ts/components/conversation/media-gallery/MediaGallery'); } = require('../../ts/components/conversation/media-gallery/MediaGallery');
@ -223,6 +226,7 @@ exports.setup = (options = {}) => {
MainHeader, MainHeader,
MemberList, MemberList,
CreateGroupDialog, CreateGroupDialog,
UpdateGroupDialog,
MediaGallery, MediaGallery,
Message, Message,
MessageBody, MessageBody,

@ -211,5 +211,9 @@
const dialog = new Whisper.CreateGroupDialogView(); const dialog = new Whisper.CreateGroupDialogView();
this.el.append(dialog.el); this.el.append(dialog.el);
}, },
showUpdateGroupDialog(groupConvo) {
const dialog = new Whisper.UpdateGroupDialogView(groupConvo);
this.el.append(dialog.el);
},
}); });
})(); })();

@ -268,6 +268,10 @@
onMoveToInbox: () => { onMoveToInbox: () => {
this.model.setArchived(false); this.model.setArchived(false);
}, },
onUpdateGroup: () => {
window.Whisper.events.trigger('updateGroup', this.model);
},
}; };
}; };
this.titleView = new Whisper.ReactWrapperView({ this.titleView = new Whisper.ReactWrapperView({

@ -7,17 +7,58 @@
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
Whisper.CreateGroupDialogView = Whisper.View.extend({ Whisper.CreateGroupDialogView = Whisper.View.extend({
templateName: 'group-creation-template',
className: 'loki-dialog modal', className: 'loki-dialog modal',
initialize() { initialize() {
this.titleText = i18n('createGroupDialogTitle'); this.titleText = i18n('createGroupDialogTitle');
this.okText = i18n('ok'); this.okText = i18n('ok');
this.cancelText = i18n('cancel'); this.cancelText = i18n('cancel');
this.close = this.close.bind(this); this.close = this.close.bind(this);
const convos = window.getConversations().models;
let allMembers = convos.filter(d => !!d);
allMembers = allMembers.filter(d => d.isFriend() && d.isPrivate());
allMembers = _.uniq(allMembers, true, d => d.id);
this.membersToShow = allMembers;
this.$el.focus(); this.$el.focus();
this.render(); this.render();
}, },
render() { render() {
this.dialogView = new Whisper.ReactWrapperView({
className: 'create-group-dialog',
Component: window.Signal.Components.CreateGroupDialog,
props: {
titleText: this.titleText,
okText: this.okText,
cancelText: this.cancelText,
friendList: this.membersToShow,
onClose: this.close,
},
});
this.$el.append(this.dialogView.el);
return this;
},
close() {
this.remove();
},
});
Whisper.UpdateGroupDialogView = Whisper.View.extend({
templateName: 'group-creation-template',
className: 'loki-dialog modal',
initialize(groupConvo) {
this.groupName = groupConvo.get('name');
this.conversation = groupConvo;
this.titleText = `${i18n('updateGroupDialogTitle')}: ${this.groupName}`;
this.okText = i18n('ok');
this.cancelText = i18n('cancel');
this.close = this.close.bind(this);
this.onSubmit = this.onSubmit.bind(this);
const convos = getInboxCollection().models; const convos = getInboxCollection().models;
let allMembers = convos.filter(d => !!d); let allMembers = convos.filter(d => !!d);
@ -25,21 +66,42 @@
allMembers = allMembers.filter(d => d.isPrivate()); allMembers = allMembers.filter(d => d.isPrivate());
allMembers = _.uniq(allMembers, true, d => d.id); allMembers = _.uniq(allMembers, true, d => d.id);
// only give members that are not already in the group
const existingMembers = groupConvo.get('members');
this.membersToShow = allMembers.filter(
d => !_.some(existingMembers, x => x === d.id)
);
this.$el.focus();
this.render();
},
render() {
this.dialogView = new Whisper.ReactWrapperView({ this.dialogView = new Whisper.ReactWrapperView({
className: 'create-group-dialog', className: 'create-group-dialog',
Component: window.Signal.Components.CreateGroupDialog, Component: window.Signal.Components.UpdateGroupDialog,
props: { props: {
titleText: this.titleText, titleText: this.titleText,
groupName: this.groupName,
okText: this.okText, okText: this.okText,
cancelText: this.cancelText, cancelText: this.cancelText,
friendList: allMembers, friendList: this.membersToShow,
onClose: this.close, onClose: this.close,
onSubmit: this.onSubmit,
}, },
}); });
this.$el.append(this.dialogView.el); this.$el.append(this.dialogView.el);
return this; return this;
}, },
onSubmit(newGroupName, newMembers) {
const allMembers = window.Lodash.concat(
newMembers,
this.conversation.get('members')
);
const groupId = this.conversation.get('id');
window.doUpdateGroup(groupId, newGroupName, allMembers);
},
close() { close() {
this.remove(); this.remove();
}, },

@ -1081,10 +1081,9 @@ MessageReceiver.prototype.extend({
let profile = null; let profile = null;
if (message.profile) { if (message.profile) {
profile = JSON.parse(message.profile.encodeJSON()); profile = JSON.parse(message.profile.encodeJSON());
// Update the conversation
await conversation.setLokiProfile(profile);
} }
// Update the conversation
await conversation.setLokiProfile(profile);
} }
if (friendRequest && isMe) { if (friendRequest && isMe) {
@ -1542,6 +1541,8 @@ MessageReceiver.prototype.extend({
} else if (decrypted.flags & FLAGS.PROFILE_KEY_UPDATE) { } else if (decrypted.flags & FLAGS.PROFILE_KEY_UPDATE) {
decrypted.body = null; decrypted.body = null;
decrypted.attachments = []; decrypted.attachments = [];
} else if (decrypted.flags & FLAGS.BACKGROUND_FRIEND_REQUEST) {
// do nothing
} else if (decrypted.flags !== 0) { } else if (decrypted.flags !== 0) {
throw new Error('Unknown flags in message'); throw new Error('Unknown flags in message');
} }

@ -382,7 +382,23 @@ MessageSender.prototype = {
); );
numbers.forEach(number => { numbers.forEach(number => {
this.queueJobForNumber(number, () => outgoing.sendToNumber(number)); // Note: if we are sending a private group message, we make our best to
// ensure we have signal protocol sessions with every member, but if we
// fail, let's at least send messages to those members with which we do:
const haveSession = _.some(
textsecure.storage.protocol.sessions,
s => s.number === number
);
if (
haveSession ||
options.isPublic ||
options.messageType === 'friend-request'
) {
this.queueJobForNumber(number, () => outgoing.sendToNumber(number));
} else {
window.log.error(`No session for number: ${number}`);
}
}); });
}, },
@ -854,6 +870,11 @@ MessageSender.prototype = {
options options
) { ) {
const profile = this.getOurProfile(); const profile = this.getOurProfile();
const flags = options.backgroundFriendReq
? textsecure.protobuf.DataMessage.Flags.BACKGROUND_FRIEND_REQUEST
: undefined;
return this.sendMessage( return this.sendMessage(
{ {
recipients: [number], recipients: [number],
@ -866,6 +887,7 @@ MessageSender.prototype = {
expireTimer, expireTimer,
profileKey, profileKey,
profile, profile,
flags,
}, },
options options
); );
@ -1000,22 +1022,6 @@ MessageSender.prototype = {
return this.sendMessage(attrs, options); return this.sendMessage(attrs, options);
}, },
createGroup(targetNumbers, id, name, avatar, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(id);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = targetNumbers;
proto.group.name = name;
// TODO: Add adding attachmentPointer once we support avatars
// (see git history)
return this.sendGroupProto(targetNumbers, proto, Date.now(), options).then(
() => proto.group.id
);
},
updateGroup(groupId, name, avatar, targetNumbers, options) { updateGroup(groupId, name, avatar, targetNumbers, options) {
const proto = new textsecure.protobuf.DataMessage(); const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext(); proto.group = new textsecure.protobuf.GroupContext();
@ -1027,6 +1033,8 @@ MessageSender.prototype = {
return this.makeAttachmentPointer(avatar).then(attachment => { return this.makeAttachmentPointer(avatar).then(attachment => {
proto.group.avatar = attachment; proto.group.avatar = attachment;
// TODO: re-enable this once we have attachments
proto.group.avatar = null;
return this.sendGroupProto( return this.sendGroupProto(
targetNumbers, targetNumbers,
proto, proto,
@ -1163,7 +1171,6 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
this.resetSession = sender.resetSession.bind(sender); this.resetSession = sender.resetSession.bind(sender);
this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender); this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender);
this.sendTypingMessage = sender.sendTypingMessage.bind(sender); this.sendTypingMessage = sender.sendTypingMessage.bind(sender);
this.createGroup = sender.createGroup.bind(sender);
this.updateGroup = sender.updateGroup.bind(sender); this.updateGroup = sender.updateGroup.bind(sender);
this.addNumberToGroup = sender.addNumberToGroup.bind(sender); this.addNumberToGroup = sender.addNumberToGroup.bind(sender);
this.setGroupName = sender.setGroupName.bind(sender); this.setGroupName = sender.setGroupName.bind(sender);

@ -94,9 +94,10 @@ message CallMessage {
message DataMessage { message DataMessage {
enum Flags { enum Flags {
END_SESSION = 1; END_SESSION = 1;
EXPIRATION_TIMER_UPDATE = 2; EXPIRATION_TIMER_UPDATE = 2;
PROFILE_KEY_UPDATE = 4; PROFILE_KEY_UPDATE = 4;
BACKGROUND_FRIEND_REQUEST = 256;
} }
message Quote { message Quote {

@ -15,6 +15,14 @@
font-size: large; font-size: large;
text-align: center; text-align: center;
} }
.no-friends {
text-align: center;
}
.hidden {
display: none;
}
} }
.friend-selection-list { .friend-selection-list {
@ -29,7 +37,7 @@
min-width: 20px; min-width: 20px;
} }
.hidden { .invisible {
visibility: hidden; visibility: hidden;
} }
} }

@ -62,6 +62,8 @@ interface Props {
onCopyPublicKey: () => void; onCopyPublicKey: () => void;
onUpdateGroup: () => void;
i18n: LocalizerType; i18n: LocalizerType;
} }
@ -222,6 +224,7 @@ export class ConversationHeader extends React.Component<Props> {
onDeleteMessages, onDeleteMessages,
onDeleteContact, onDeleteContact,
onCopyPublicKey, onCopyPublicKey,
onUpdateGroup,
} = this.props; } = this.props;
return ( return (
@ -229,6 +232,7 @@ export class ConversationHeader extends React.Component<Props> {
{this.renderPublicMenuItems()} {this.renderPublicMenuItems()}
<MenuItem onClick={onCopyPublicKey}>{i18n('copyPublicKey')}</MenuItem> <MenuItem onClick={onCopyPublicKey}>{i18n('copyPublicKey')}</MenuItem>
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem> <MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
<MenuItem onClick={onUpdateGroup}>{i18n('updateGroup')}</MenuItem>
{!isMe && isClosable ? ( {!isMe && isClosable ? (
!isPublic ? ( !isPublic ? (
<MenuItem onClick={onDeleteContact}> <MenuItem onClick={onDeleteContact}>

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { MemberList, Contact } from './MemberList'; import { Contact, MemberList } from './MemberList';
declare global { declare global {
interface Window { interface Window {
@ -12,13 +12,13 @@ interface Props {
titleText: string; titleText: string;
okText: string; okText: string;
cancelText: string; cancelText: string;
friendList: any[]; friendList: Array<any>;
i18n: any; i18n: any;
onClose: any; onClose: any;
} }
interface State { interface State {
friendList: Contact[]; friendList: Array<Contact>;
groupName: string; groupName: string;
} }
@ -58,50 +58,14 @@ export class CreateGroupDialog extends React.Component<Props, State> {
window.addEventListener('keyup', this.onKeyUp); window.addEventListener('keyup', this.onKeyUp);
} }
private onKeyUp(event: any) {
switch (event.key) {
case 'Enter':
this.onClickOK();
break;
case 'Esc':
case 'Escape':
this.closeDialog();
break;
default:
break;
}
}
private closeDialog() {
window.removeEventListener('keyup', this.onKeyUp);
this.props.onClose();
}
private onMemberClicked(selected: any) {
this.setState(state => {
const updatedFriends = this.state.friendList.map(member => {
if (member.id === selected.id) {
return { ...member, checkmarked: !member.checkmarked };
} else {
return member;
}
});
return {
...state,
friendList: updatedFriends,
};
});
}
public onClickOK() { public onClickOK() {
const members = this.state.friendList const members = this.state.friendList
.filter(d => d.checkmarked) .filter(d => d.checkmarked)
.map(d => d.id); .map(d => d.id);
if (!this.state.groupName.trim()) { if (!this.state.groupName.trim()) {
console.error('Group name cannot be empty!'); // TODO: show error message
// console.error('Group name cannot be empty!');
return; return;
} }
@ -110,17 +74,6 @@ export class CreateGroupDialog extends React.Component<Props, State> {
this.closeDialog(); this.closeDialog();
} }
private onGroupNameChanged(event: any) {
event.persist();
this.setState(state => {
return {
...state,
groupName: event.target.value,
};
});
}
public render() { public render() {
const titleText = this.props.titleText; const titleText = this.props.titleText;
const okText = this.props.okText; const okText = this.props.okText;
@ -137,8 +90,9 @@ export class CreateGroupDialog extends React.Component<Props, State> {
value={this.state.groupName} value={this.state.groupName}
onChange={this.onGroupNameChanged} onChange={this.onGroupNameChanged}
tabIndex={0} tabIndex={0}
required required={true}
autoFocus autoFocus={true}
aria-required={true}
/> />
<div className="friend-selection-list"> <div className="friend-selection-list">
<MemberList <MemberList
@ -149,14 +103,61 @@ export class CreateGroupDialog extends React.Component<Props, State> {
/> />
</div> </div>
<div className="buttons"> <div className="buttons">
<button className="cancel" tabIndex={2} onClick={this.closeDialog}> <button className="cancel" tabIndex={0} onClick={this.closeDialog}>
{cancelText} {cancelText}
</button> </button>
<button className="ok" tabIndex={1} onClick={this.onClickOK}> <button className="ok" tabIndex={0} onClick={this.onClickOK}>
{okText} {okText}
</button> </button>
</div> </div>
</div> </div>
); );
} }
private onGroupNameChanged(event: any) {
event.persist();
this.setState(state => {
return {
...state,
groupName: event.target.value,
};
});
}
private onKeyUp(event: any) {
switch (event.key) {
case 'Enter':
this.onClickOK();
break;
case 'Esc':
case 'Escape':
this.closeDialog();
break;
default:
}
}
private closeDialog() {
window.removeEventListener('keyup', this.onKeyUp);
this.props.onClose();
}
private onMemberClicked(selected: any) {
this.setState(state => {
const updatedFriends = this.state.friendList.map(member => {
if (member.id === selected.id) {
return { ...member, checkmarked: !member.checkmarked };
} else {
return member;
}
});
return {
...state,
friendList: updatedFriends,
};
});
}
} }

@ -33,7 +33,7 @@ class MemberItem extends React.Component<MemberItemProps> {
const checkMarkClass = this.props.checkmarked const checkMarkClass = this.props.checkmarked
? 'check-mark' ? 'check-mark'
: classNames('check-mark', 'hidden'); : classNames('check-mark', 'invisible');
return ( return (
<div <div
@ -73,7 +73,7 @@ class MemberItem extends React.Component<MemberItemProps> {
} }
interface MemberListProps { interface MemberListProps {
members: Contact[]; members: Array<Contact>;
selected: any; selected: any;
onMemberClicked: any; onMemberClicked: any;
i18n: any; i18n: any;

@ -0,0 +1,163 @@
import React from 'react';
import classNames from 'classnames';
import { Contact, MemberList } from './MemberList';
interface Props {
titleText: string;
groupName: string;
okText: string;
cancelText: string;
friendList: Array<any>;
i18n: any;
onSubmit: any;
onClose: any;
}
interface State {
friendList: Array<Contact>;
groupName: string;
}
export class UpdateGroupDialog extends React.Component<Props, State> {
constructor(props: any) {
super(props);
this.onMemberClicked = this.onMemberClicked.bind(this);
this.onClickOK = this.onClickOK.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.closeDialog = this.closeDialog.bind(this);
this.onGroupNameChanged = this.onGroupNameChanged.bind(this);
let friends = this.props.friendList;
friends = friends.map(d => {
const lokiProfile = d.getLokiProfile();
const name = lokiProfile ? lokiProfile.displayName : 'Anonymous';
return {
id: d.id,
authorPhoneNumber: d.id,
authorProfileName: name,
selected: false,
authorName: name, // different from ProfileName?
authorColor: d.getColor(),
checkmarked: false,
};
});
this.state = {
friendList: friends,
groupName: this.props.groupName,
};
window.addEventListener('keyup', this.onKeyUp);
}
public onClickOK() {
const members = this.state.friendList
.filter(d => d.checkmarked)
.map(d => d.id);
if (!this.state.groupName.trim()) {
// TODO: show error message
// window.log.error('Group name cannot be empty!');
return;
}
this.props.onSubmit(this.state.groupName, members);
this.closeDialog();
}
public render() {
const titleText = this.props.titleText;
const okText = this.props.okText;
const cancelText = this.props.cancelText;
const noFriendsClasses =
this.state.friendList.length === 0
? 'no-friends'
: classNames('no-friends', 'hidden');
return (
<div className="content">
<p className="titleText">{titleText}</p>
<input
type="text"
id="group-name"
className="group-name"
placeholder="Group Name"
value={this.state.groupName}
onChange={this.onGroupNameChanged}
tabIndex={0}
required={true}
aria-required={true}
autoFocus={true}
/>
<div className="friend-selection-list">
<MemberList
members={this.state.friendList}
selected={{}}
i18n={this.props.i18n}
onMemberClicked={this.onMemberClicked}
/>
</div>
<p className={noFriendsClasses}>(no friends to add)</p>
<div className="buttons">
<button className="cancel" tabIndex={0} onClick={this.closeDialog}>
{cancelText}
</button>
<button className="ok" tabIndex={0} onClick={this.onClickOK}>
{okText}
</button>
</div>
</div>
);
}
private onKeyUp(event: any) {
switch (event.key) {
case 'Enter':
this.onClickOK();
break;
case 'Esc':
case 'Escape':
this.closeDialog();
break;
default:
}
}
private closeDialog() {
window.removeEventListener('keyup', this.onKeyUp);
this.props.onClose();
}
private onMemberClicked(selected: any) {
this.setState(state => {
const updatedFriends = this.state.friendList.map(member => {
if (member.id === selected.id) {
return { ...member, checkmarked: !member.checkmarked };
} else {
return member;
}
});
return {
...state,
friendList: updatedFriends,
};
});
}
private onGroupNameChanged(event: any) {
event.persist();
this.setState(state => {
return {
...state,
groupName: event.target.value,
};
});
}
}

@ -235,6 +235,7 @@ function showArchivedConversations() {
function createNewGroup() { function createNewGroup() {
// Not sure how much of this is necessary: // Not sure how much of this is necessary:
trigger('createNewGroup'); trigger('createNewGroup');
return { return {
type: 'CREATE_NEW_GROUP', type: 'CREATE_NEW_GROUP',
payload: null, payload: null,

Loading…
Cancel
Save