From 0df5214979eb7a7d10897a1369d5e5fcc7e73787 Mon Sep 17 00:00:00 2001 From: Maxim Shishmarev Date: Mon, 21 Oct 2019 17:16:24 +1100 Subject: [PATCH] Limit small private groups to 10 members --- _locales/en/messages.json | 9 +++ js/views/conversation_view.js | 4 +- js/views/create_group_dialog_view.js | 3 + preload.js | 3 + stylesheets/_mentions.scss | 4 ++ ts/components/LeftPane.tsx | 8 --- ts/components/MainHeader.tsx | 7 +++ .../conversation/ConversationHeader.tsx | 21 ++++++- .../conversation/CreateGroupDialog.tsx | 49 +++++++++++----- .../conversation/UpdateGroupDialog.tsx | 58 +++++++++++++++---- ts/state/ducks/conversations.ts | 11 ---- 11 files changed, 131 insertions(+), 46 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ff3f6e59b..0889a38be 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1989,6 +1989,12 @@ "Title for the dialog box used to connect to a new public server" }, + "createPrivateGroup": { + "message": "Create Private Group", + "description": + "Button action that the user can click to show a dialog for creating a new private group chat" + }, + "seedViewTitle": { "message": "Please save the seed below in a safe location. They can be used to restore your account if you lose access or migrate to a new device.", @@ -2119,5 +2125,8 @@ "emptyGroupNameError": { "message": "Group Name cannot be empty", "description": "Error message displayed on empty group name" + }, + "maxGroupMembersError": { + "message": "Max number of members for small group chats is: " } } diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 137016358..40b09edd1 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -196,6 +196,8 @@ ? Whisper.ExpirationTimerOptions.getName(expireTimer || 0) : null; + const members = this.model.get('members') || []; + return { id: this.model.id, name: this.model.getName(), @@ -213,7 +215,7 @@ isOnline: this.model.isOnline(), isArchived: this.model.get('isArchived'), isPublic: this.model.isPublic(), - + members, expirationSettingName, showBackButton: Boolean(this.panels && this.panels.length), timerOptions: Whisper.ExpirationTimerOptions.map(item => ({ diff --git a/js/views/create_group_dialog_view.js b/js/views/create_group_dialog_view.js index 28a89c757..8c383b4b0 100644 --- a/js/views/create_group_dialog_view.js +++ b/js/views/create_group_dialog_view.js @@ -112,6 +112,8 @@ d => !_.some(existingMembers, x => x === d.id) ); + this.existingMembers = existingMembers; + this.$el.focus(); this.render(); }, @@ -124,6 +126,7 @@ groupName: this.groupName, okText: this.okText, cancelText: this.cancelText, + existingMembers: this.existingMembers, friendList: this.membersToShow, onClose: this.close, onSubmit: this.onSubmit, diff --git a/preload.js b/preload.js index d0d167143..06a345fb5 100644 --- a/preload.js +++ b/preload.js @@ -459,3 +459,6 @@ if (config.environment === 'test') { window.shortenPubkey = pubkey => `(...${pubkey.substring(pubkey.length - 6)})`; window.pubkeyPattern = /@[a-fA-F0-9]{64,66}\b/g; + +// Limited due to the proof-of-work requirement +window.SMALL_GROUP_SIZE_LIMIT = 10; diff --git a/stylesheets/_mentions.scss b/stylesheets/_mentions.scss index 22e7d727f..df04b95ac 100644 --- a/stylesheets/_mentions.scss +++ b/stylesheets/_mentions.scss @@ -25,6 +25,10 @@ } } +.member-preview { + margin-left: 10px; +} + .create-group-dialog { .content { max-width: 100% !important; diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 04cb955d0..e77273683 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -27,8 +27,6 @@ export interface Props { options: { regionCode: string } ) => void; - createNewGroup: () => void; - openConversationInternal: (id: string, messageId?: string) => void; showArchivedConversations: () => void; showInbox: () => void; @@ -265,12 +263,6 @@ export class LeftPane extends React.Component {
{showArchived ? this.renderArchivedHeader() : renderMainHeader()}
- {this.renderList()} ); diff --git a/ts/components/MainHeader.tsx b/ts/components/MainHeader.tsx index 21396b616..51c9276c3 100644 --- a/ts/components/MainHeader.tsx +++ b/ts/components/MainHeader.tsx @@ -341,6 +341,13 @@ export class MainHeader extends React.Component { trigger('showAddServerDialog'); }, }, + { + id: 'createPrivateGroup', + name: i18n('createPrivateGroup'), + onClick: () => { + trigger('createNewGroup'); + }, + }, ]; const passItem = (type: string) => ({ diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 41f737e69..e79181f1a 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -31,6 +31,8 @@ interface Props { isArchived: boolean; isPublic: boolean; + members: Array; + expirationSettingName?: string; showBackButton: boolean; timerOptions: Array; @@ -262,9 +264,11 @@ export class ConversationHeader extends React.Component { } public render() { - const { id } = this.props; + const { id, isGroup, isPublic } = this.props; const triggerId = `conversation-${id}`; + const isPrivateGroup = isGroup && !isPublic; + return (
{this.renderBackButton()} @@ -272,6 +276,7 @@ export class ConversationHeader extends React.Component {
{this.renderAvatar()} {this.renderTitle()} + {isPrivateGroup ? this.renderMemberCount() : null}
{this.renderExpirationLength()} @@ -281,6 +286,20 @@ export class ConversationHeader extends React.Component { ); } + private renderMemberCount() { + const memberCount = this.props.members.length; + + if (memberCount === 0) { + return null; + } + + const wordForm = memberCount === 1 ? 'member' : 'members'; + + return ( + {`(${memberCount} ${wordForm})`} + ); + } + private renderPublicMenuItems() { const { i18n, diff --git a/ts/components/conversation/CreateGroupDialog.tsx b/ts/components/conversation/CreateGroupDialog.tsx index 43a34bf7c..4eeba8dff 100644 --- a/ts/components/conversation/CreateGroupDialog.tsx +++ b/ts/components/conversation/CreateGroupDialog.tsx @@ -6,6 +6,7 @@ declare global { interface Window { Lodash: any; doCreateGroup: any; + SMALL_GROUP_SIZE_LIMIT: number; } } @@ -22,6 +23,7 @@ interface State { friendList: Array; groupName: string; errorDisplayed: boolean; + errorMessage: string; } export class CreateGroupDialog extends React.Component { @@ -56,6 +58,8 @@ export class CreateGroupDialog extends React.Component { friendList: friends, groupName: '', errorDisplayed: false, + // if empty, the initial height is 0, which is not desirable + errorMessage: 'placeholder', }; window.addEventListener('keyup', this.onKeyUp); @@ -67,7 +71,7 @@ export class CreateGroupDialog extends React.Component { .map(d => d.id); if (!this.state.groupName.trim()) { - this.onShowError(); + this.onShowError(this.props.i18n('emptyGroupNameError')); return; } @@ -78,11 +82,12 @@ export class CreateGroupDialog extends React.Component { } public render() { - const titleText = this.props.titleText; + const checkMarkedCount = this.getMemberCount(); + + const titleText = `${this.props.titleText} (Members: ${checkMarkedCount})`; const okText = this.props.okText; const cancelText = this.props.cancelText; - const errorMsg = this.props.i18n('emptyGroupNameError'); const errorMessageClasses = classNames( 'error-message', this.state.errorDisplayed ? 'error-shown' : 'error-faded' @@ -91,7 +96,7 @@ export class CreateGroupDialog extends React.Component { return (

{titleText}

-

{errorMsg}

+

{this.state.errorMessage}

{ ); } - private onShowError() { + private onShowError(msg: string) { if (this.state.errorDisplayed) { return; } this.setState({ errorDisplayed: true, + errorMessage: msg, }); setTimeout(() => { @@ -164,6 +170,11 @@ export class CreateGroupDialog extends React.Component { } } + private getMemberCount() { + // Add 1 to include yourself + return this.state.friendList.filter(d => d.checkmarked).length + 1; + } + private closeDialog() { window.removeEventListener('keyup', this.onKeyUp); @@ -171,15 +182,27 @@ export class CreateGroupDialog extends React.Component { } 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; - } - }); + const updatedFriends = this.state.friendList.map(member => { + if (member.id === selected.id) { + return { ...member, checkmarked: !member.checkmarked }; + } else { + return member; + } + }); + if ( + updatedFriends.filter(d => d.checkmarked).length > + window.SMALL_GROUP_SIZE_LIMIT - 1 + ) { + const msg = `${this.props.i18n('maxGroupMembersError')} ${ + window.SMALL_GROUP_SIZE_LIMIT + }`; + this.onShowError(msg); + + return; + } + + this.setState(state => { return { ...state, friendList: updatedFriends, diff --git a/ts/components/conversation/UpdateGroupDialog.tsx b/ts/components/conversation/UpdateGroupDialog.tsx index ccc3ee441..58f0cbdec 100644 --- a/ts/components/conversation/UpdateGroupDialog.tsx +++ b/ts/components/conversation/UpdateGroupDialog.tsx @@ -2,12 +2,20 @@ import React from 'react'; import classNames from 'classnames'; import { Contact, MemberList } from './MemberList'; +declare global { + interface Window { + SMALL_GROUP_SIZE_LIMIT: number; + } +} + interface Props { titleText: string; groupName: string; okText: string; cancelText: string; + // friends not in the group friendList: Array; + existingMembers: Array; i18n: any; onSubmit: any; onClose: any; @@ -17,6 +25,7 @@ interface State { friendList: Array; groupName: string; errorDisplayed: boolean; + errorMessage: string; } export class UpdateGroupDialog extends React.Component { @@ -49,6 +58,7 @@ export class UpdateGroupDialog extends React.Component { friendList: friends, groupName: this.props.groupName, errorDisplayed: false, + errorMessage: 'placeholder', }; window.addEventListener('keyup', this.onKeyUp); @@ -60,7 +70,7 @@ export class UpdateGroupDialog extends React.Component { .map(d => d.id); if (!this.state.groupName.trim()) { - this.onShowError(); + this.onShowError(this.props.i18n('emptyGroupNameError')); return; } @@ -71,7 +81,10 @@ export class UpdateGroupDialog extends React.Component { } public render() { - const titleText = this.props.titleText; + const checkMarkedCount = this.getMemberCount(); + + const titleText = `${this.props.titleText} (Members: ${checkMarkedCount})`; + const okText = this.props.okText; const cancelText = this.props.cancelText; @@ -80,7 +93,7 @@ export class UpdateGroupDialog extends React.Component { ? 'no-friends' : classNames('no-friends', 'hidden'); - const errorMsg = this.props.i18n('emptyGroupNameError'); + const errorMsg = this.state.errorMessage; const errorMessageClasses = classNames( 'error-message', this.state.errorDisplayed ? 'error-shown' : 'error-faded' @@ -123,13 +136,14 @@ export class UpdateGroupDialog extends React.Component { ); } - private onShowError() { + private onShowError(msg: string) { if (this.state.errorDisplayed) { return; } this.setState({ errorDisplayed: true, + errorMessage: msg, }); setTimeout(() => { @@ -152,6 +166,13 @@ export class UpdateGroupDialog extends React.Component { } } + private getMemberCount() { + return ( + this.props.existingMembers.length + + this.state.friendList.filter(d => d.checkmarked).length + ); + } + private closeDialog() { window.removeEventListener('keyup', this.onKeyUp); @@ -159,15 +180,28 @@ export class UpdateGroupDialog extends React.Component { } 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; - } - }); + const updatedFriends = this.state.friendList.map(member => { + if (member.id === selected.id) { + return { ...member, checkmarked: !member.checkmarked }; + } else { + return member; + } + }); + + const newMemberCount = + this.props.existingMembers.length + + updatedFriends.filter(d => d.checkmarked).length; + + if (newMemberCount > window.SMALL_GROUP_SIZE_LIMIT - 1) { + const msg = `${this.props.i18n('maxGroupMembersError')} ${ + window.SMALL_GROUP_SIZE_LIMIT + }`; + this.onShowError(msg); + return; + } + + this.setState(state => { return { ...state, friendList: updatedFriends, diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 54706604a..ab189d13c 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -137,7 +137,6 @@ export const actions = { openConversationExternal, showInbox, showArchivedConversations, - createNewGroup, }; function conversationAdded( @@ -232,16 +231,6 @@ function showArchivedConversations() { }; } -function createNewGroup() { - // Not sure how much of this is necessary: - trigger('createNewGroup'); - - return { - type: 'CREATE_NEW_GROUP', - payload: null, - }; -} - // Reducer function getEmptyState(): ConversationsStateType {