Add group admins and the ability to remove members from private groups

pull/565/head
Maxim Shishmarev 6 years ago
parent 0df5214979
commit 7c05939f55

@ -309,6 +309,11 @@
"description": "description":
"Displayed when a user can't send a message because they have left the group" "Displayed when a user can't send a message because they have left the group"
}, },
"youGotKickedFromGroup": {
"message": "You were removed from the group",
"description":
"Displayed when a user can't send a message because they have left the group"
},
"scrollDown": { "scrollDown": {
"message": "Scroll to bottom of conversation", "message": "Scroll to bottom of conversation",
"description": "description":
@ -1839,6 +1844,28 @@
} }
} }
}, },
"kickedFromTheGroup": {
"message": "$name$ was removed from the group",
"description":
"Shown in the conversation history when a single person is removed from the group",
"placeholders": {
"name": {
"content": "$1",
"example": "Alice"
}
}
},
"multipleKickedFromTheGroup": {
"message": "$names$ were removed from the group",
"description":
"Shown in the conversation history when more than one person is removed from the group",
"placeholders": {
"names": {
"content": "$1",
"example": "Alice, Bob"
}
}
},
"friendRequestPending": { "friendRequestPending": {
"message": "Friend request", "message": "Friend request",
"description": "description":
@ -2128,5 +2155,8 @@
}, },
"maxGroupMembersError": { "maxGroupMembersError": {
"message": "Max number of members for small group chats is: " "message": "Max number of members for small group chats is: "
},
"nonAdminDeleteMember": {
"message": "Only group admin can remove members!"
} }
} }

@ -701,17 +701,25 @@
}, },
}; };
await onMessageReceived(ev); const convo = await ConversationController.getOrCreateAndWait(
groupId,
'group'
);
const avatar = ''; const avatar = '';
const options = {}; const options = {};
textsecure.messaging.updateGroup(
const recipients = _.union(convo.get('members'), members);
await onMessageReceived(ev);
convo.updateGroup({
groupId, groupId,
groupName, groupName,
avatar, avatar,
recipients,
members, members,
options options,
); });
}; };
window.doCreateGroup = async (groupName, members) => { window.doCreateGroup = async (groupName, members) => {
@ -722,10 +730,13 @@
const ourKey = textsecure.storage.user.getNumber(); const ourKey = textsecure.storage.user.getNumber();
const allMembers = [ourKey, ...members];
ev.groupDetails = { ev.groupDetails = {
id: groupId, id: groupId,
name: groupName, name: groupName,
members: [ourKey, ...members], members: allMembers,
recipients: allMembers,
active: true, active: true,
expireTimer: 0, expireTimer: 0,
avatar: '', avatar: '',
@ -748,6 +759,8 @@
window.friends.friendRequestStatusEnum.friends window.friends.friendRequestStatusEnum.friends
); );
convo.updateGroupAdmins([ourKey]);
appView.openConversation(groupId, {}); appView.openConversation(groupId, {});
}; };
@ -1005,7 +1018,6 @@
messageReceiver.addEventListener('delivery', onDeliveryReceipt); messageReceiver.addEventListener('delivery', onDeliveryReceipt);
messageReceiver.addEventListener('contact', onContactReceived); messageReceiver.addEventListener('contact', onContactReceived);
messageReceiver.addEventListener('group', onGroupReceived); messageReceiver.addEventListener('group', onGroupReceived);
window.addEventListener('group', onGroupReceived);
messageReceiver.addEventListener('sent', onSentMessage); messageReceiver.addEventListener('sent', onSentMessage);
messageReceiver.addEventListener('readSync', onReadSync); messageReceiver.addEventListener('readSync', onReadSync);
messageReceiver.addEventListener('read', onReadReceipt); messageReceiver.addEventListener('read', onReadReceipt);

@ -84,6 +84,8 @@
unlockTimestamp: null, // Timestamp used for expiring friend requests. unlockTimestamp: null, // Timestamp used for expiring friend requests.
sessionResetStatus: SessionResetEnum.none, sessionResetStatus: SessionResetEnum.none,
swarmNodes: [], swarmNodes: [],
groupAdmins: [],
isKickedFromGroup: false,
isOnline: false, isOnline: false,
}; };
}, },
@ -671,6 +673,10 @@
this.trigger('disable:input', true); this.trigger('disable:input', true);
return; return;
} }
if (this.get('isKickedFromGroup')) {
this.trigger('disable:input', true);
return;
}
if (!this.isPrivate() && this.get('left')) { if (!this.isPrivate() && this.get('left')) {
this.trigger('disable:input', true); this.trigger('disable:input', true);
this.trigger('change:placeholder', 'left-group'); this.trigger('change:placeholder', 'left-group');
@ -715,6 +721,12 @@
this.updateTextInputState(); this.updateTextInputState();
} }
}, },
async updateGroupAdmins(groupAdmins) {
this.set({ groupAdmins });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
},
async respondToAllFriendRequests(options) { async respondToAllFriendRequests(options) {
const { response, status, direction = null } = options; const { response, status, direction = null } = options;
// Ignore if no response supplied // Ignore if no response supplied
@ -1933,6 +1945,7 @@
this.get('name'), this.get('name'),
this.get('avatar'), this.get('avatar'),
this.get('members'), this.get('members'),
groupUpdate.recipients,
options options
) )
) )
@ -2710,6 +2723,23 @@
return; return;
} }
// For groups, block typing messages from non-members (e.g. from kicked members)
if (this.get('type') === 'group') {
const knownMembers = this.get('members');
if (knownMembers) {
const fromMember = knownMembers.indexOf(sender) !== -1;
if (!fromMember) {
window.log.warn(
'Blocking typing messages from a non-member: ',
sender
);
return;
}
}
}
const identifier = `${sender}.${senderDevice}`; const identifier = `${sender}.${senderDevice}`;
this.contactTypingTimers = this.contactTypingTimers || {}; this.contactTypingTimers = this.contactTypingTimers || {};

@ -204,8 +204,12 @@
return i18n('leftTheGroup', this.getNameForNumber(groupUpdate.left)); return i18n('leftTheGroup', this.getNameForNumber(groupUpdate.left));
} }
if (groupUpdate.kicked === 'You') {
return i18n('youGotKickedFromGroup');
}
const messages = []; const messages = [];
if (!groupUpdate.name && !groupUpdate.joined) { if (!groupUpdate.name && !groupUpdate.joined && !groupUpdate.kicked) {
messages.push(i18n('updatedTheGroup')); messages.push(i18n('updatedTheGroup'));
} }
if (groupUpdate.name) { if (groupUpdate.name) {
@ -223,6 +227,18 @@
} }
} }
if (groupUpdate.kicked && groupUpdate.kicked.length) {
const names = _.map(
groupUpdate.kicked,
this.getNameForNumber.bind(this)
);
if (names.length > 1) {
messages.push(i18n('multipleKickedFromTheGroup', names.join(', ')));
} else {
messages.push(i18n('kickedFromTheGroup', names[0]));
}
}
return messages.join(', '); return messages.join(', ');
} }
if (this.isEndSession()) { if (this.isEndSession()) {
@ -462,6 +478,23 @@
}); });
} }
if (groupUpdate.kicked === 'You') {
changes.push({
type: 'kicked',
isMe: true,
});
} else if (groupUpdate.kicked) {
changes.push({
type: 'kicked',
contacts: _.map(
Array.isArray(groupUpdate.kicked)
? groupUpdate.kicked
: [groupUpdate.kicked],
phoneNumber => this.findAndFormatContact(phoneNumber)
),
});
}
if (groupUpdate.left === 'You') { if (groupUpdate.left === 'You') {
changes.push({ changes.push({
type: 'remove', type: 'remove',
@ -1705,16 +1738,65 @@
const conversation = ConversationController.get(conversationId); const conversation = ConversationController.get(conversationId);
// NOTE: we use friends status to tell if this is
// the creation of the group (initial update)
const newGroup = !conversation.isFriend();
const knownMembers = conversation.get('members');
if (!newGroup && knownMembers) {
const fromMember = knownMembers.indexOf(source) !== -1;
if (!fromMember) {
window.log.warn(`Ignoring group message from non-member: ${source}`);
confirm();
return null;
}
}
if ( if (
initialMessage.group && initialMessage.group &&
initialMessage.group.members && initialMessage.group.members &&
initialMessage.group.type === GROUP_TYPES.UPDATE initialMessage.group.type === GROUP_TYPES.UPDATE
) { ) {
// Note: this might be called more times than necessary if (newGroup) {
conversation.updateGroupAdmins(initialMessage.group.admins);
conversation.setFriendRequestStatus( conversation.setFriendRequestStatus(
window.friends.friendRequestStatusEnum.friends window.friends.friendRequestStatusEnum.friends
); );
}
const fromAdmin =
conversation.get('groupAdmins').indexOf(source) !== -1;
if (!fromAdmin) {
// Make sure the message is not removing members / renaming the group
const nameChanged =
conversation.get('name') !== initialMessage.group.name;
if (nameChanged) {
window.log.warn(
'Non-admin attempts to change the name of the group'
);
}
const membersMissing =
_.difference(
conversation.get('members'),
initialMessage.group.members
).length > 0;
if (membersMissing) {
window.log.warn('Non-admin attempts to remove group members');
}
const messageAllowed = !nameChanged && !membersMissing;
if (!messageAllowed) {
confirm();
return null;
}
}
// For every member, see if we need to establish a session: // For every member, see if we need to establish a session:
initialMessage.group.members.forEach(memberPubKey => { initialMessage.group.members.forEach(memberPubKey => {
const haveSession = _.some( const haveSession = _.some(
@ -1797,10 +1879,7 @@
attributes = { attributes = {
...attributes, ...attributes,
name: dataMessage.group.name, name: dataMessage.group.name,
members: _.union( members: dataMessage.group.members,
dataMessage.group.members,
conversation.get('members')
),
}; };
groupUpdate = groupUpdate =
@ -1808,12 +1887,12 @@
_.pick(dataMessage.group, 'name', 'avatar') _.pick(dataMessage.group, 'name', 'avatar')
) || {}; ) || {};
const difference = _.difference( const addedMembers = _.difference(
attributes.members, attributes.members,
conversation.get('members') conversation.get('members')
); );
if (difference.length > 0) { if (addedMembers.length > 0) {
groupUpdate.joined = difference; groupUpdate.joined = addedMembers;
} }
if (conversation.get('left')) { if (conversation.get('left')) {
// TODO: Maybe we shouldn't assume this message adds us: // TODO: Maybe we shouldn't assume this message adds us:
@ -1821,6 +1900,30 @@
window.log.warn('re-added to a left group'); window.log.warn('re-added to a left group');
attributes.left = false; attributes.left = false;
} }
if (attributes.isKickedFromGroup) {
// Assume somebody re-invited us since we received this update
attributes.isKickedFromGroup = false;
}
// Check if anyone got kicked:
const removedMembers = _.difference(
conversation.get('members'),
attributes.members
);
if (removedMembers.length > 0) {
if (
removedMembers.indexOf(
textsecure.storage.user.getNumber()
) !== -1
) {
groupUpdate.kicked = 'You';
attributes.isKickedFromGroup = true;
} else {
groupUpdate.kicked = removedMembers;
}
}
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) { } else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
if (source === textsecure.storage.user.getNumber()) { if (source === textsecure.storage.user.getNumber()) {
attributes.left = true; attributes.left = true;

@ -2,6 +2,8 @@
const electron = require('electron'); const electron = require('electron');
// TODO: this results in poor readability, would be
// much better to implicitly call with `_`.
const { const {
cloneDeep, cloneDeep,
forEach, forEach,
@ -9,11 +11,12 @@ const {
isFunction, isFunction,
isObject, isObject,
map, map,
merge,
set, set,
omit, omit,
} = require('lodash'); } = require('lodash');
const _ = require('lodash');
const { base64ToArrayBuffer, arrayBufferToBase64 } = require('./crypto'); const { base64ToArrayBuffer, arrayBufferToBase64 } = require('./crypto');
const MessageType = require('./types/message'); const MessageType = require('./types/message');
@ -662,17 +665,6 @@ async function getAllSessions(id) {
// Conversation // Conversation
function setifyProperty(data, propertyName) {
if (!data) {
return data;
}
const returnData = { ...data };
if (Array.isArray(returnData[propertyName])) {
returnData[propertyName] = new Set(returnData[propertyName]);
}
return returnData;
}
async function getSwarmNodesByPubkey(pubkey) { async function getSwarmNodesByPubkey(pubkey) {
return channels.getSwarmNodesByPubkey(pubkey); return channels.getSwarmNodesByPubkey(pubkey);
} }
@ -701,13 +693,14 @@ async function updateConversation(id, data, { Conversation }) {
if (!existing) { if (!existing) {
throw new Error(`Conversation ${id} does not exist!`); throw new Error(`Conversation ${id} does not exist!`);
} }
const setData = setifyProperty(data, 'swarmNodes');
const setExisting = setifyProperty(existing.attributes, 'swarmNodes');
const merged = merge({}, setExisting, setData); const merged = _.merge({}, existing.attributes, data);
if (merged.swarmNodes instanceof Set) {
merged.swarmNodes = Array.from(merged.swarmNodes); // Merging is a really bad idea and not what we want here, e.g.
} // it will take a union of old and new members and that's not
// what we want for member deletion, so:
merged.members = data.members;
merged.swarmNodes = data.swarmNodes;
// Don't save the online status of the object // Don't save the online status of the object
const cleaned = omit(merged, 'isOnline'); const cleaned = omit(merged, 'isOnline');

@ -2548,11 +2548,12 @@
.models.filter(d => d.isPrivate()); .models.filter(d => d.isPrivate());
const memberConvos = members const memberConvos = members
.map(m => privateConvos.find(c => c.id === m)) .map(m => privateConvos.find(c => c.id === m))
.filter(c => !!c); .filter(c => !!c && c.getLokiProfile());
allMembers = memberConvos.map(m => ({
id: m.id, allMembers = memberConvos.map(c => ({
authorPhoneNumber: m.id, id: c.id,
authorProfileName: m.getLokiProfile().displayName, authorPhoneNumber: c.id,
authorProfileName: c.getLokiProfile().displayName,
})); }));
} }

@ -1,4 +1,4 @@
/* global Whisper, i18n, getInboxCollection _ */ /* global Whisper, i18n, textsecure, _ */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function() { (function() {
@ -17,7 +17,9 @@
const convos = window.getConversations().models; const convos = window.getConversations().models;
let allMembers = convos.filter(d => !!d); let allMembers = convos.filter(d => !!d);
allMembers = allMembers.filter(d => d.isFriend() && d.isPrivate()); allMembers = allMembers.filter(
d => d.isFriend() && d.isPrivate() && !d.isMe()
);
allMembers = _.uniq(allMembers, true, d => d.id); allMembers = _.uniq(allMembers, true, d => d.id);
this.membersToShow = allMembers; this.membersToShow = allMembers;
@ -99,18 +101,22 @@
this.close = this.close.bind(this); this.close = this.close.bind(this);
this.onSubmit = this.onSubmit.bind(this); this.onSubmit = this.onSubmit.bind(this);
const convos = getInboxCollection().models; const ourPK = textsecure.storage.user.getNumber();
this.isAdmin = groupConvo.get('groupAdmins').indexOf(ourPK) !== -1;
const convos = window.getConversations().models;
let allMembers = convos.filter(d => !!d); let allMembers = convos.filter(d => !!d);
allMembers = allMembers.filter(d => d.isFriend()); allMembers = allMembers.filter(
allMembers = allMembers.filter(d => d.isPrivate()); d => d.isFriend() && d.isPrivate() && !d.isMe()
);
allMembers = _.uniq(allMembers, true, d => d.id); allMembers = _.uniq(allMembers, true, d => d.id);
this.friendList = allMembers;
// only give members that are not already in the group // only give members that are not already in the group
const existingMembers = groupConvo.get('members'); const existingMembers = groupConvo.get('members');
this.membersToShow = allMembers.filter(
d => !_.some(existingMembers, x => x === d.id)
);
this.existingMembers = existingMembers; this.existingMembers = existingMembers;
@ -127,7 +133,8 @@
okText: this.okText, okText: this.okText,
cancelText: this.cancelText, cancelText: this.cancelText,
existingMembers: this.existingMembers, existingMembers: this.existingMembers,
friendList: this.membersToShow, friendList: this.friendList,
isAdmin: this.isAdmin,
onClose: this.close, onClose: this.close,
onSubmit: this.onSubmit, onSubmit: this.onSubmit,
}, },
@ -137,10 +144,8 @@
return this; return this;
}, },
onSubmit(newGroupName, newMembers) { onSubmit(newGroupName, newMembers) {
const allMembers = window.Lodash.concat( const ourPK = textsecure.storage.user.getNumber();
newMembers, const allMembers = window.Lodash.concat(newMembers, [ourPK]);
this.conversation.get('members')
);
const groupId = this.conversation.get('id'); const groupId = this.conversation.get('id');
window.doUpdateGroup(groupId, newGroupName, allMembers); window.doUpdateGroup(groupId, newGroupName, allMembers);

@ -6,6 +6,7 @@
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
// TODO: remove this as unused?
Whisper.GroupUpdateView = Backbone.View.extend({ Whisper.GroupUpdateView = Backbone.View.extend({
tagName: 'div', tagName: 'div',
className: 'group-update', className: 'group-update',

@ -1022,25 +1022,25 @@ MessageSender.prototype = {
return this.sendMessage(attrs, options); return this.sendMessage(attrs, options);
}, },
updateGroup(groupId, name, avatar, targetNumbers, options) { updateGroup(groupId, name, avatar, members, recipients, 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();
proto.group.id = stringToArrayBuffer(groupId); proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name; proto.group.name = name;
proto.group.members = targetNumbers; proto.group.members = members;
const ourPK = textsecure.storage.user.getNumber();
proto.group.admins = [ourPK];
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 // TODO: re-enable this once we have attachments
proto.group.avatar = null; proto.group.avatar = null;
return this.sendGroupProto( return this.sendGroupProto(recipients, proto, Date.now(), options).then(
targetNumbers, () => proto.group.id
proto, );
Date.now(),
options
).then(() => proto.group.id);
}); });
}, },

@ -337,6 +337,7 @@ message GroupContext {
optional string name = 3; optional string name = 3;
repeated string members = 4; repeated string members = 4;
optional AttachmentPointer avatar = 5; optional AttachmentPointer avatar = 5;
repeated string admins = 6;
} }
message ContactDetails { message ContactDetails {

@ -75,6 +75,7 @@
.friend-selection-list { .friend-selection-list {
max-height: 240px; max-height: 240px;
overflow-y: scroll; overflow-y: scroll;
margin: 4px;
.check-mark { .check-mark {
float: right; float: right;
@ -87,6 +88,14 @@
.invisible { .invisible {
visibility: hidden; visibility: hidden;
} }
.existing-member {
color: green;
}
.existing-member-kicked {
color: red;
}
} }
.dark-theme { .dark-theme {

@ -89,6 +89,13 @@ class Mention extends React.Component<MentionProps, MentionState> {
return d.id === this.props.convoId; return d.id === this.props.convoId;
}); });
if (!thisConvo) {
// If this gets triggered, is is likely because we deleted the conversation
this.clearOurInterval();
return;
}
if (thisConvo.isPublic()) { if (thisConvo.isPublic()) {
// TODO: make this work for other public chats as well // TODO: make this work for other public chats as well
groupMembers = window.lokiPublicChatAPI groupMembers = window.lokiPublicChatAPI
@ -106,10 +113,14 @@ class Mention extends React.Component<MentionProps, MentionState> {
.map((m: any) => privateConvos.find((c: any) => c.id === m)) .map((m: any) => privateConvos.find((c: any) => c.id === m))
.filter((c: any) => !!c); .filter((c: any) => !!c);
groupMembers = memberConversations.map((m: any) => { groupMembers = memberConversations.map((m: any) => {
const name = m.getLokiProfile()
? m.getLokiProfile().displayName
: m.attributes.displayName;
return { return {
id: m.id, id: m.id,
authorPhoneNumber: m.id, authorPhoneNumber: m.id,
authorProfileName: m.getLokiProfile().displayName, authorProfileName: name,
}; };
}); });
} }

@ -15,7 +15,7 @@ interface Contact {
} }
interface Change { interface Change {
type: 'add' | 'remove' | 'name' | 'general'; type: 'add' | 'remove' | 'name' | 'general' | 'kicked';
isMe: boolean; isMe: boolean;
newName?: string; newName?: string;
contacts?: Array<Contact>; contacts?: Array<Contact>;
@ -78,6 +78,21 @@ export class GroupNotification extends React.Component<Props> {
contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup'; contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup';
return <Intl i18n={i18n} id={leftKey} components={[people]} />; return <Intl i18n={i18n} id={leftKey} components={[people]} />;
case 'kicked':
if (isMe) {
return i18n('youGotKickedFromGroup');
}
if (!contacts || !contacts.length) {
throw new Error('Group update is missing contacts');
}
const kickedKey =
contacts.length > 1
? 'multipleKickedFromTheGroup'
: 'kickedFromTheGroup';
return <Intl i18n={i18n} id={kickedKey} components={[people]} />;
case 'general': case 'general':
return i18n('updatedTheGroup'); return i18n('updatedTheGroup');
default: default:

@ -11,10 +11,12 @@ export interface Contact {
authorColor: any; authorColor: any;
authorAvatarPath: string; authorAvatarPath: string;
checkmarked: boolean; checkmarked: boolean;
existingMember: boolean;
} }
interface MemberItemProps { interface MemberItemProps {
member: Contact; member: Contact;
selected: boolean; selected: boolean;
existingMember: boolean;
onClicked: any; onClicked: any;
i18n: any; i18n: any;
checkmarked: boolean; checkmarked: boolean;
@ -30,10 +32,41 @@ class MemberItem extends React.Component<MemberItemProps> {
const name = this.props.member.authorProfileName; const name = this.props.member.authorProfileName;
const pubkey = this.props.member.authorPhoneNumber; const pubkey = this.props.member.authorPhoneNumber;
const selected = this.props.selected; const selected = this.props.selected;
const existingMember = this.props.existingMember;
const checkMarkClass = this.props.checkmarked let markType: 'none' | 'kicked' | 'added' | 'existing' = 'none';
? 'check-mark'
: classNames('check-mark', 'invisible'); if (this.props.checkmarked) {
if (existingMember) {
markType = 'kicked';
} else {
markType = 'added';
}
} else {
if (existingMember) {
markType = 'existing';
} else {
markType = 'none';
}
}
const markClasses = ['check-mark'];
switch (markType) {
case 'none':
markClasses.push('invisible');
break;
case 'existing':
markClasses.push('existing-member');
break;
case 'kicked':
markClasses.push('existing-member-kicked');
break;
default:
// do nothing
}
const mark = markType === 'kicked' ? '✘' : '✔';
return ( return (
<div <div
@ -47,7 +80,7 @@ class MemberItem extends React.Component<MemberItemProps> {
{this.renderAvatar()} {this.renderAvatar()}
<span className="name-part">{name}</span> <span className="name-part">{name}</span>
<span className="pubkey-part">{pubkey}</span> <span className="pubkey-part">{pubkey}</span>
<span className={checkMarkClass}></span> <span className={classNames(markClasses)}>{mark}</span>
</div> </div>
); );
} }
@ -98,6 +131,7 @@ export class MemberList extends React.Component<MemberListProps> {
member={item} member={item}
selected={selected} selected={selected}
checkmarked={item.checkmarked} checkmarked={item.checkmarked}
existingMember={item.existingMember}
i18n={this.props.i18n} i18n={this.props.i18n}
onClicked={this.handleMemberClicked} onClicked={this.handleMemberClicked}
/> />

@ -5,6 +5,7 @@ import { Contact, MemberList } from './MemberList';
declare global { declare global {
interface Window { interface Window {
SMALL_GROUP_SIZE_LIMIT: number; SMALL_GROUP_SIZE_LIMIT: number;
Lodash: any;
} }
} }
@ -15,6 +16,7 @@ interface Props {
cancelText: string; cancelText: string;
// friends not in the group // friends not in the group
friendList: Array<any>; friendList: Array<any>;
isAdmin: boolean;
existingMembers: Array<any>; existingMembers: Array<any>;
i18n: any; i18n: any;
onSubmit: any; onSubmit: any;
@ -43,6 +45,8 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
const lokiProfile = d.getLokiProfile(); const lokiProfile = d.getLokiProfile();
const name = lokiProfile ? lokiProfile.displayName : 'Anonymous'; const name = lokiProfile ? lokiProfile.displayName : 'Anonymous';
const existingMember = this.props.existingMembers.indexOf(d.id) !== -1;
return { return {
id: d.id, id: d.id,
authorPhoneNumber: d.id, authorPhoneNumber: d.id,
@ -51,6 +55,7 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
authorName: name, // different from ProfileName? authorName: name, // different from ProfileName?
authorColor: d.getColor(), authorColor: d.getColor(),
checkmarked: false, checkmarked: false,
existingMember,
}; };
}); });
@ -65,9 +70,9 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
} }
public onClickOK() { public onClickOK() {
const members = this.state.friendList const members = this.getWouldBeMembers(this.state.friendList).map(
.filter(d => d.checkmarked) d => d.id
.map(d => d.id); );
if (!this.state.groupName.trim()) { if (!this.state.groupName.trim()) {
this.onShowError(this.props.i18n('emptyGroupNameError')); this.onShowError(this.props.i18n('emptyGroupNameError'));
@ -81,7 +86,7 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
} }
public render() { public render() {
const checkMarkedCount = this.getMemberCount(); const checkMarkedCount = this.getMemberCount(this.state.friendList);
const titleText = `${this.props.titleText} (Members: ${checkMarkedCount})`; const titleText = `${this.props.titleText} (Members: ${checkMarkedCount})`;
@ -109,6 +114,7 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
className="group-name" className="group-name"
placeholder="Group Name" placeholder="Group Name"
value={this.state.groupName} value={this.state.groupName}
disabled={!this.props.isAdmin}
onChange={this.onGroupNameChanged} onChange={this.onGroupNameChanged}
tabIndex={0} tabIndex={0}
required={true} required={true}
@ -166,11 +172,20 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
} }
} }
private getMemberCount() { // Return members that would comprise the group given the
// current state in `users`
private getWouldBeMembers(users: Array<Contact>) {
return users.filter(d => {
return ( return (
this.props.existingMembers.length + (d.existingMember && !d.checkmarked) ||
this.state.friendList.filter(d => d.checkmarked).length (!d.existingMember && d.checkmarked)
); );
});
}
private getMemberCount(users: Array<Contact>) {
// Adding one to include ourselves
return this.getWouldBeMembers(users).length + 1;
} }
private closeDialog() { private closeDialog() {
@ -180,6 +195,12 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
} }
private onMemberClicked(selected: any) { private onMemberClicked(selected: any) {
if (selected.existingMember && !this.props.isAdmin) {
this.onShowError(this.props.i18n('nonAdminDeleteMember'));
return;
}
const updatedFriends = this.state.friendList.map(member => { const updatedFriends = this.state.friendList.map(member => {
if (member.id === selected.id) { if (member.id === selected.id) {
return { ...member, checkmarked: !member.checkmarked }; return { ...member, checkmarked: !member.checkmarked };
@ -188,11 +209,9 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
} }
}); });
const newMemberCount = const newMemberCount = this.getMemberCount(updatedFriends);
this.props.existingMembers.length +
updatedFriends.filter(d => d.checkmarked).length;
if (newMemberCount > window.SMALL_GROUP_SIZE_LIMIT - 1) { if (newMemberCount > window.SMALL_GROUP_SIZE_LIMIT) {
const msg = `${this.props.i18n('maxGroupMembersError')} ${ const msg = `${this.props.i18n('maxGroupMembersError')} ${
window.SMALL_GROUP_SIZE_LIMIT window.SMALL_GROUP_SIZE_LIMIT
}`; }`;

Loading…
Cancel
Save