diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 47de3b572..9ecd3917c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1752,6 +1752,18 @@ "message": "You cannot remove this user as they are the creator of the group.", "description": "Toast description when the user tries to remove the creator from a closed group v2 member list." }, + "userNeedsToHaveJoined": { + "message": "User needs to have joined", + "description": "Toast title when the user tries to remove the creator from a closed group v2 member list." + }, + "userNeedsToHaveJoinedDesc": { + "message": "Remember that this user needs to have already joined the server for this ADD to work.", + "description": "Toast description when the user adds a moderator for an open group." + }, + "addToTheListBelow": { + "message": "Add to the list below", + "description": "Button action to add a moderator by pubkey" + }, "noContactsForGroup": { "message": "You don't have any contacts yet", "androidKey": "activity_create_closed_group_empty_state_message" diff --git a/js/models/conversations.d.ts b/js/models/conversations.d.ts index eed53b9de..082bfc856 100644 --- a/js/models/conversations.d.ts +++ b/js/models/conversations.d.ts @@ -65,7 +65,7 @@ export interface ConversationModel isRss: () => boolean; isBlocked: () => boolean; isClosable: () => boolean; - isModerator: (id?: string) => boolean; + isModerator: (id: string) => boolean; throttledBumpTyping: () => void; messageCollection: Backbone.Collection; diff --git a/js/models/conversations.js b/js/models/conversations.js index 03861a967..56f78dab6 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1827,6 +1827,9 @@ if (!this.isPublic()) { return false; } + if (!pubKey) { + throw new Error('isModerator() pubKey is falsy'); + } const moderators = this.get('moderators'); return Array.isArray(moderators) && moderators.includes(pubKey); }, diff --git a/js/views/app_view.js b/js/views/app_view.js index 82fb65d1c..c1639333e 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -23,7 +23,7 @@ openInbox: 'openInbox', }, applyRtl() { - const rtlLocales = ['fa']; + const rtlLocales = ['fa', 'ar', 'he']; const loc = window.i18n.getLocale(); if (rtlLocales.includes(loc)) { diff --git a/js/views/moderators_add_dialog_view.js b/js/views/moderators_add_dialog_view.js index d2d777dce..5c1887a95 100644 --- a/js/views/moderators_add_dialog_view.js +++ b/js/views/moderators_add_dialog_view.js @@ -21,17 +21,20 @@ const modPubKeys = await this.channelAPI.getModerators(); // private contacts (not you) that aren't already moderators - const contacts = convo.filter( - d => - !!d && - d.isPrivate() && - !d.isBlocked() && - !d.isMe() && - !modPubKeys.includes(d.id) - ); + const contacts = window + .getConversationController() + .getConversations() + .filter( + d => + !!d && + d.isPrivate() && + !d.isBlocked() && + !d.isMe() && + !modPubKeys.includes(d.id) + ); this.contacts = contacts; - this.this.theme = convo.theme; + this.theme = convo.theme; this.$el.focus(); this.render(); @@ -56,11 +59,16 @@ this.remove(); }, async onSubmit(pubKeys) { - log.info(`asked to add ${pubKeys}`); + log.info(`asked to add moderators: ${pubKeys}`); + window.libsession.Utils.ToastUtils.pushUserNeedsToHaveJoined(); + const res = await this.channelAPI.serverAPI.addModerators(pubKeys); if (res !== true) { // we have errors, deal with them... // how? + window.log.warn('failed to add moderators:', res); + } else { + window.log.info(`${pubKeys} added as moderators...`); } }, }); diff --git a/js/views/moderators_remove_dialog_view.js b/js/views/moderators_remove_dialog_view.js index 06120dc77..46974a441 100644 --- a/js/views/moderators_remove_dialog_view.js +++ b/js/views/moderators_remove_dialog_view.js @@ -56,10 +56,15 @@ this.remove(); }, async onSubmit(pubKeys) { + window.log.info(`asked to remove moderators ${pubKeys}`); + const res = await this.channelAPI.serverAPI.removeModerators(pubKeys); if (res !== true) { // we have errors, deal with them... // how? + window.log.warn('failed to remove moderators:', res); + } else { + window.log.info(`${pubKeys} removed from moderators...`); } }, }); diff --git a/stylesheets/_modal.scss b/stylesheets/_modal.scss index d0bab63db..ab3bcf8eb 100644 --- a/stylesheets/_modal.scss +++ b/stylesheets/_modal.scss @@ -33,6 +33,9 @@ .content { max-width: 100% !important; } + .contact-selection-list { + width: 100%; + } .buttons { margin: 8px; diff --git a/ts/components/DevicePairingDialog.tsx b/ts/components/DevicePairingDialog.tsx index 4befa72fb..17e4dc67c 100644 --- a/ts/components/DevicePairingDialog.tsx +++ b/ts/components/DevicePairingDialog.tsx @@ -4,8 +4,6 @@ import { QRCode } from 'react-qr-svg'; import { SessionModal } from './session/SessionModal'; import { SessionButton, SessionButtonColor } from './session/SessionButton'; import { SessionSpinner } from './session/SessionSpinner'; -import { toast } from 'react-toastify'; -import { SessionToast, SessionToastType } from './session/SessionToast'; import { ToastUtils } from '../session/utils'; import { DefaultTheme } from 'styled-components'; import { ConversationController } from '../session/conversations'; diff --git a/ts/components/EditProfileDialog.tsx b/ts/components/EditProfileDialog.tsx index a58f3fda8..6fabff939 100644 --- a/ts/components/EditProfileDialog.tsx +++ b/ts/components/EditProfileDialog.tsx @@ -256,7 +256,7 @@ export class EditProfileDialog extends React.Component { private renderAvatar() { const { avatar, profileName } = this.state; const { pubkey } = this.props; - const userName = name || profileName || pubkey; + const userName = profileName || pubkey; return ( @@ -264,8 +264,6 @@ export class EditProfileDialog extends React.Component { } private onNameEdited(event: any) { - event.persist(); - const newName = event.target.value.replace(window.displayNameRegex, ''); this.setState(state => { diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index b8f5c7c1e..59abe4e00 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -44,7 +44,7 @@ interface Props { isPrivate: boolean; isPublic: boolean; isRss: boolean; - amMod: boolean; + isAdmin: boolean; // We might not always have the full list of members, // e.g. for open groups where we could have thousands diff --git a/ts/components/conversation/ModeratorsAddDialog.tsx b/ts/components/conversation/ModeratorsAddDialog.tsx index 37335fc49..99b7b21a5 100644 --- a/ts/components/conversation/ModeratorsAddDialog.tsx +++ b/ts/components/conversation/ModeratorsAddDialog.tsx @@ -1,14 +1,17 @@ import React from 'react'; import { Contact, MemberList } from './MemberList'; import { cleanSearchTerm } from '../../util/cleanSearchTerm'; -import { DefaultTheme } from 'styled-components'; +import { + SessionButton, + SessionButtonColor, + SessionButtonType, +} from '../session/SessionButton'; interface Props { contactList: Array; chatName: string; onSubmit: any; onClose: any; - // theme: DefaultTheme; } interface State { @@ -117,7 +120,7 @@ export class AddModeratorsDialog extends React.Component { } public render() { - const i18n = window.i18n; + const { i18n } = window; const hasContacts = this.state.contactList.length !== 0; @@ -136,9 +139,12 @@ export class AddModeratorsDialog extends React.Component { dir="auto" onChange={this.updateSearchBound} /> - +

From friends:

@@ -152,13 +158,19 @@ export class AddModeratorsDialog extends React.Component {
{hasContacts ? null :

{i18n('noContactsToAdd')}

} -
- - +
+ +
); diff --git a/ts/components/conversation/ModeratorsRemoveDialog.tsx b/ts/components/conversation/ModeratorsRemoveDialog.tsx index 75f20d8d6..74df7d400 100644 --- a/ts/components/conversation/ModeratorsRemoveDialog.tsx +++ b/ts/components/conversation/ModeratorsRemoveDialog.tsx @@ -1,4 +1,9 @@ import React from 'react'; +import { + SessionButton, + SessionButtonColor, + SessionButtonType, +} from '../session/SessionButton'; import { Contact, MemberList } from './MemberList'; interface Props { @@ -69,13 +74,19 @@ export class RemoveModeratorsDialog extends React.Component { {hasMods ? null :

{i18n('noModeratorsToRemove')}

} -
- - +
+ +
); diff --git a/ts/components/conversation/UpdateGroupNameDialog.tsx b/ts/components/conversation/UpdateGroupNameDialog.tsx index dcc8cfb1b..e92445135 100644 --- a/ts/components/conversation/UpdateGroupNameDialog.tsx +++ b/ts/components/conversation/UpdateGroupNameDialog.tsx @@ -157,12 +157,11 @@ class UpdateGroupNameDialogInner extends React.Component { } private onGroupNameChanged(event: any) { - event.persist(); - + const groupName = event.target.value; this.setState(state => { return { ...state, - groupName: event.target.value, + groupName, }; }); } diff --git a/ts/components/session/SessionInput.tsx b/ts/components/session/SessionInput.tsx index 00e151d18..212236689 100644 --- a/ts/components/session/SessionInput.tsx +++ b/ts/components/session/SessionInput.tsx @@ -72,7 +72,6 @@ export class SessionInput extends React.PureComponent { this.updateInputValue(e); }} onKeyPress={event => { - event.persist(); if (event.key === 'Enter' && this.props.onEnterPressed) { this.props.onEnterPressed(); } diff --git a/ts/components/session/SessionPasswordModal.tsx b/ts/components/session/SessionPasswordModal.tsx index 7496e1a03..52cceeff1 100644 --- a/ts/components/session/SessionPasswordModal.tsx +++ b/ts/components/session/SessionPasswordModal.tsx @@ -285,14 +285,18 @@ class SessionPasswordModalInner extends React.Component { if (event.key === 'Enter') { return this.setPassword(this.props.onOk); } - this.setState({ currentPasswordEntered: event.target.value }); + const currentPasswordEntered = event.target.value; + + this.setState({ currentPasswordEntered }); } private async onPasswordConfirmInput(event: any) { if (event.key === 'Enter') { return this.setPassword(this.props.onOk); } - this.setState({ currentPasswordConfirmEntered: event.target.value }); + const currentPasswordConfirmEntered = event.target.value; + + this.setState({ currentPasswordConfirmEntered }); } } diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index 2607ae168..33fe02905 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -444,7 +444,7 @@ export class SessionConversation extends React.Component { isPrivate: conversation.isPrivate(), isPublic: conversation.isPublic(), isRss: conversation.isRss(), - amMod: conversation.isModerator( + isAdmin: conversation.isModerator( window.storage.get('primaryDevicePubKey') ), members, @@ -484,7 +484,7 @@ export class SessionConversation extends React.Component { }, onUpdateGroupName: () => { - conversation.onUpdateGroupName(); + window.Whisper.events.trigger('updateGroupName', conversation); }, onBlockUser: () => { @@ -549,12 +549,14 @@ export class SessionConversation extends React.Component { const conversation = ConversationController.getInstance().getOrThrow( conversationKey ); + const ourPrimary = window.storage.get('primaryDevicePubKey'); - const ourPK = window.textsecure.storage.user.getNumber(); const members = conversation.get('members') || []; const isAdmin = conversation.isMediumGroup() ? true - : conversation.get('groupAdmins')?.includes(ourPK); + : conversation.isPublic() + ? conversation.isModerator(ourPrimary) + : false; return { id: conversation.id, @@ -563,7 +565,6 @@ export class SessionConversation extends React.Component { phoneNumber: conversation.getNumber(), profileName: conversation.getProfileName(), avatarPath: conversation.getAvatarPath(), - amMod: conversation.isModerator(), isKickedFromGroup: conversation.get('isKickedFromGroup'), left: conversation.get('left'), isGroup: !conversation.isPrivate(), @@ -601,7 +602,13 @@ export class SessionConversation extends React.Component { onLeaveGroup: () => { window.Whisper.events.trigger('leaveGroup', conversation); }, + onAddModerators: () => { + window.Whisper.events.trigger('addModerators', conversation); + }, + onRemoveModerators: () => { + window.Whisper.events.trigger('removeModerators', conversation); + }, onShowLightBox: (lightBoxOptions = {}) => { this.setState({ lightBoxOptions }); }, diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index ce70bd3de..a488b32a6 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -29,7 +29,6 @@ interface Props { timerOptions: Array; isPublic: boolean; isAdmin: boolean; - amMod: boolean; isKickedFromGroup: boolean; left: boolean; isBlocked: boolean; @@ -40,6 +39,8 @@ interface Props { onInviteContacts: () => void; onLeaveGroup: () => void; onUpdateGroupName: () => void; + onAddModerators: () => void; + onRemoveModerators: () => void; onUpdateGroupMembers: () => void; onShowLightBox: (options: any) => void; onSetDisappearingMessages: (seconds: number) => void; @@ -247,7 +248,6 @@ class SessionRightPanel extends React.Component { left, isPublic, isAdmin, - amMod, isBlocked, isGroup, } = this.props; @@ -273,10 +273,9 @@ class SessionRightPanel extends React.Component { }; }); - const showUpdateGroupNameButton = - isPublic && !commonNoShow - ? amMod && !commonNoShow - : isAdmin && !commonNoShow; + const showUpdateGroupNameButton = isAdmin && !commonNoShow; + const showAddRemoveModeratorsButton = isAdmin && !commonNoShow && isPublic; + const showUpdateGroupMembersButton = !isPublic && !commonNoShow && isAdmin; return ( @@ -305,6 +304,25 @@ class SessionRightPanel extends React.Component { {isPublic ? window.i18n('editGroup') : window.i18n('editGroupName')} )} + {showAddRemoveModeratorsButton && ( + <> +
+ {window.i18n('addModerators')} +
+
+ {window.i18n('removeModerators')} +
+ + )} + {showUpdateGroupMembersButton && (
; isPrivate: boolean; isBlocked: boolean; @@ -54,7 +54,7 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { isRss, isGroup, isKickedFromGroup, - amMod, + isAdmin, timerOptions, isBlocked, isPrivate, @@ -115,19 +115,19 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { {getCopyMenuItem(isPublic, isRss, isGroup, onCopyPublicKey, window.i18n)} {getDeleteMessagesMenuItem(isPublic, onDeleteMessages, window.i18n)} {getAddModeratorsMenuItem( - amMod, + isAdmin, isKickedFromGroup, onAddModerators, window.i18n )} {getRemoveModeratorsMenuItem( - amMod, + isAdmin, isKickedFromGroup, onRemoveModerators, window.i18n )} {getUpdateGroupNameMenuItem( - amMod, + isAdmin, isKickedFromGroup, left, onUpdateGroupName, diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 052ef399a..8ae690853 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -75,25 +75,25 @@ function showDeleteContact( } function showAddModerators( - amMod: boolean, + isAdmin: boolean, isKickedFromGroup: boolean ): boolean { - return !isKickedFromGroup && amMod; + return !isKickedFromGroup && isAdmin; } function showRemoveModerators( - amMod: boolean, + isAdmin: boolean, isKickedFromGroup: boolean ): boolean { - return !isKickedFromGroup && amMod; + return !isKickedFromGroup && isAdmin; } function showUpdateGroupName( - amMod: boolean, + isAdmin: boolean, isKickedFromGroup: boolean, left: boolean ): boolean { - return !isKickedFromGroup && !left && amMod; + return !isKickedFromGroup && !left && isAdmin; } function showLeaveGroup( @@ -174,7 +174,7 @@ export function getLeaveGroupMenuItem( } export function getUpdateGroupNameMenuItem( - amMod: boolean | undefined, + isAdmin: boolean | undefined, isKickedFromGroup: boolean | undefined, left: boolean | undefined, action: any, @@ -182,7 +182,7 @@ export function getUpdateGroupNameMenuItem( ): JSX.Element | null { if ( showUpdateGroupName( - Boolean(amMod), + Boolean(isAdmin), Boolean(isKickedFromGroup), Boolean(left) ) @@ -193,24 +193,24 @@ export function getUpdateGroupNameMenuItem( } export function getRemoveModeratorsMenuItem( - amMod: boolean | undefined, + isAdmin: boolean | undefined, isKickedFromGroup: boolean | undefined, action: any, i18n: LocalizerType ): JSX.Element | null { - if (showRemoveModerators(Boolean(amMod), Boolean(isKickedFromGroup))) { + if (showRemoveModerators(Boolean(isAdmin), Boolean(isKickedFromGroup))) { return {i18n('removeModerators')}; } return null; } export function getAddModeratorsMenuItem( - amMod: boolean | undefined, + isAdmin: boolean | undefined, isKickedFromGroup: boolean | undefined, action: any, i18n: LocalizerType ): JSX.Element | null { - if (showAddModerators(Boolean(amMod), Boolean(isKickedFromGroup))) { + if (showAddModerators(Boolean(isAdmin), Boolean(isKickedFromGroup))) { return {i18n('addModerators')}; } return null; diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx index 4ce058f5a..4217f9e11 100644 --- a/ts/session/utils/Toast.tsx +++ b/ts/session/utils/Toast.tsx @@ -259,3 +259,11 @@ export function pushCannotRemoveCreatorFromGroup() { window.i18n('cannotRemoveCreatorFromGroupDesc') ); } + +export function pushUserNeedsToHaveJoined() { + pushToastInfo( + 'userNeedsToHaveJoined', + window.i18n('userNeedsToHaveJoined'), + window.i18n('userNeedsToHaveJoinedDesc') + ); +}