Merge pull request #1437 from oxen-io/clearnet

Fix Moderators dialogs
This commit is contained in:
Audric Ackermann 2021-01-19 09:16:46 +11:00 committed by GitHub
commit 334f371367
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 157 additions and 72 deletions

View file

@ -1752,6 +1752,18 @@
"message": "You cannot remove this user as they are the creator of the group.", "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." "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": { "noContactsForGroup": {
"message": "You don't have any contacts yet", "message": "You don't have any contacts yet",
"androidKey": "activity_create_closed_group_empty_state_message" "androidKey": "activity_create_closed_group_empty_state_message"

View file

@ -65,7 +65,7 @@ export interface ConversationModel
isRss: () => boolean; isRss: () => boolean;
isBlocked: () => boolean; isBlocked: () => boolean;
isClosable: () => boolean; isClosable: () => boolean;
isModerator: (id?: string) => boolean; isModerator: (id: string) => boolean;
throttledBumpTyping: () => void; throttledBumpTyping: () => void;
messageCollection: Backbone.Collection<MessageModel>; messageCollection: Backbone.Collection<MessageModel>;

View file

@ -1827,6 +1827,9 @@
if (!this.isPublic()) { if (!this.isPublic()) {
return false; return false;
} }
if (!pubKey) {
throw new Error('isModerator() pubKey is falsy');
}
const moderators = this.get('moderators'); const moderators = this.get('moderators');
return Array.isArray(moderators) && moderators.includes(pubKey); return Array.isArray(moderators) && moderators.includes(pubKey);
}, },

View file

@ -23,7 +23,7 @@
openInbox: 'openInbox', openInbox: 'openInbox',
}, },
applyRtl() { applyRtl() {
const rtlLocales = ['fa']; const rtlLocales = ['fa', 'ar', 'he'];
const loc = window.i18n.getLocale(); const loc = window.i18n.getLocale();
if (rtlLocales.includes(loc)) { if (rtlLocales.includes(loc)) {

View file

@ -21,17 +21,20 @@
const modPubKeys = await this.channelAPI.getModerators(); const modPubKeys = await this.channelAPI.getModerators();
// private contacts (not you) that aren't already moderators // private contacts (not you) that aren't already moderators
const contacts = convo.filter( const contacts = window
d => .getConversationController()
!!d && .getConversations()
d.isPrivate() && .filter(
!d.isBlocked() && d =>
!d.isMe() && !!d &&
!modPubKeys.includes(d.id) d.isPrivate() &&
); !d.isBlocked() &&
!d.isMe() &&
!modPubKeys.includes(d.id)
);
this.contacts = contacts; this.contacts = contacts;
this.this.theme = convo.theme; this.theme = convo.theme;
this.$el.focus(); this.$el.focus();
this.render(); this.render();
@ -56,11 +59,16 @@
this.remove(); this.remove();
}, },
async onSubmit(pubKeys) { 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); const res = await this.channelAPI.serverAPI.addModerators(pubKeys);
if (res !== true) { if (res !== true) {
// we have errors, deal with them... // we have errors, deal with them...
// how? // how?
window.log.warn('failed to add moderators:', res);
} else {
window.log.info(`${pubKeys} added as moderators...`);
} }
}, },
}); });

View file

@ -56,10 +56,15 @@
this.remove(); this.remove();
}, },
async onSubmit(pubKeys) { async onSubmit(pubKeys) {
window.log.info(`asked to remove moderators ${pubKeys}`);
const res = await this.channelAPI.serverAPI.removeModerators(pubKeys); const res = await this.channelAPI.serverAPI.removeModerators(pubKeys);
if (res !== true) { if (res !== true) {
// we have errors, deal with them... // we have errors, deal with them...
// how? // how?
window.log.warn('failed to remove moderators:', res);
} else {
window.log.info(`${pubKeys} removed from moderators...`);
} }
}, },
}); });

View file

@ -33,6 +33,9 @@
.content { .content {
max-width: 100% !important; max-width: 100% !important;
} }
.contact-selection-list {
width: 100%;
}
.buttons { .buttons {
margin: 8px; margin: 8px;

View file

@ -4,8 +4,6 @@ import { QRCode } from 'react-qr-svg';
import { SessionModal } from './session/SessionModal'; import { SessionModal } from './session/SessionModal';
import { SessionButton, SessionButtonColor } from './session/SessionButton'; import { SessionButton, SessionButtonColor } from './session/SessionButton';
import { SessionSpinner } from './session/SessionSpinner'; import { SessionSpinner } from './session/SessionSpinner';
import { toast } from 'react-toastify';
import { SessionToast, SessionToastType } from './session/SessionToast';
import { ToastUtils } from '../session/utils'; import { ToastUtils } from '../session/utils';
import { DefaultTheme } from 'styled-components'; import { DefaultTheme } from 'styled-components';
import { ConversationController } from '../session/conversations'; import { ConversationController } from '../session/conversations';

View file

@ -256,7 +256,7 @@ export class EditProfileDialog extends React.Component<Props, State> {
private renderAvatar() { private renderAvatar() {
const { avatar, profileName } = this.state; const { avatar, profileName } = this.state;
const { pubkey } = this.props; const { pubkey } = this.props;
const userName = name || profileName || pubkey; const userName = profileName || pubkey;
return ( return (
<Avatar avatarPath={avatar} name={userName} size={80} pubkey={pubkey} /> <Avatar avatarPath={avatar} name={userName} size={80} pubkey={pubkey} />
@ -264,8 +264,6 @@ export class EditProfileDialog extends React.Component<Props, State> {
} }
private onNameEdited(event: any) { private onNameEdited(event: any) {
event.persist();
const newName = event.target.value.replace(window.displayNameRegex, ''); const newName = event.target.value.replace(window.displayNameRegex, '');
this.setState(state => { this.setState(state => {

View file

@ -44,7 +44,7 @@ interface Props {
isPrivate: boolean; isPrivate: boolean;
isPublic: boolean; isPublic: boolean;
isRss: boolean; isRss: boolean;
amMod: boolean; isAdmin: boolean;
// We might not always have the full list of members, // We might not always have the full list of members,
// e.g. for open groups where we could have thousands // e.g. for open groups where we could have thousands

View file

@ -1,14 +1,17 @@
import React from 'react'; import React from 'react';
import { Contact, MemberList } from './MemberList'; import { Contact, MemberList } from './MemberList';
import { cleanSearchTerm } from '../../util/cleanSearchTerm'; import { cleanSearchTerm } from '../../util/cleanSearchTerm';
import { DefaultTheme } from 'styled-components'; import {
SessionButton,
SessionButtonColor,
SessionButtonType,
} from '../session/SessionButton';
interface Props { interface Props {
contactList: Array<any>; contactList: Array<any>;
chatName: string; chatName: string;
onSubmit: any; onSubmit: any;
onClose: any; onClose: any;
// theme: DefaultTheme;
} }
interface State { interface State {
@ -117,7 +120,7 @@ export class AddModeratorsDialog extends React.Component<Props, State> {
} }
public render() { public render() {
const i18n = window.i18n; const { i18n } = window;
const hasContacts = this.state.contactList.length !== 0; const hasContacts = this.state.contactList.length !== 0;
@ -136,9 +139,12 @@ export class AddModeratorsDialog extends React.Component<Props, State> {
dir="auto" dir="auto"
onChange={this.updateSearchBound} onChange={this.updateSearchBound}
/> />
<button className="add" tabIndex={0} onClick={this.add}> <SessionButton
{i18n('add')} buttonType={SessionButtonType.Brand}
</button> buttonColor={SessionButtonColor.Primary}
onClick={this.add}
text={i18n('addToTheListBelow')}
/>
</div> </div>
<div className="moderatorList"> <div className="moderatorList">
<p>From friends:</p> <p>From friends:</p>
@ -152,13 +158,19 @@ export class AddModeratorsDialog extends React.Component<Props, State> {
</div> </div>
{hasContacts ? null : <p>{i18n('noContactsToAdd')}</p>} {hasContacts ? null : <p>{i18n('noContactsToAdd')}</p>}
</div> </div>
<div className="buttons"> <div className="session-modal__button-group">
<button className="cancel" tabIndex={0} onClick={this.closeDialog}> <SessionButton
{i18n('cancel')} buttonType={SessionButtonType.Brand}
</button> buttonColor={SessionButtonColor.Secondary}
<button className="ok" tabIndex={0} onClick={this.onClickOK}> onClick={this.closeDialog}
{i18n('ok')} text={i18n('cancel')}
</button> />
<SessionButton
buttonType={SessionButtonType.BrandOutline}
buttonColor={SessionButtonColor.Green}
onClick={this.onClickOK}
text={i18n('ok')}
/>
</div> </div>
</div> </div>
); );

View file

@ -1,4 +1,9 @@
import React from 'react'; import React from 'react';
import {
SessionButton,
SessionButtonColor,
SessionButtonType,
} from '../session/SessionButton';
import { Contact, MemberList } from './MemberList'; import { Contact, MemberList } from './MemberList';
interface Props { interface Props {
@ -69,13 +74,19 @@ export class RemoveModeratorsDialog extends React.Component<Props, State> {
</div> </div>
{hasMods ? null : <p>{i18n('noModeratorsToRemove')}</p>} {hasMods ? null : <p>{i18n('noModeratorsToRemove')}</p>}
</div> </div>
<div className="buttons"> <div className="session-modal__button-group">
<button className="cancel" tabIndex={0} onClick={this.closeDialog}> <SessionButton
{i18n('cancel')} buttonType={SessionButtonType.Brand}
</button> buttonColor={SessionButtonColor.Primary}
<button className="ok" tabIndex={0} onClick={this.onClickOK}> onClick={this.closeDialog}
{i18n('ok')} text={i18n('cancel')}
</button> />
<SessionButton
buttonType={SessionButtonType.BrandOutline}
buttonColor={SessionButtonColor.Green}
onClick={this.onClickOK}
text={i18n('ok')}
/>
</div> </div>
</div> </div>
); );

View file

@ -157,12 +157,11 @@ class UpdateGroupNameDialogInner extends React.Component<Props, State> {
} }
private onGroupNameChanged(event: any) { private onGroupNameChanged(event: any) {
event.persist(); const groupName = event.target.value;
this.setState(state => { this.setState(state => {
return { return {
...state, ...state,
groupName: event.target.value, groupName,
}; };
}); });
} }

View file

@ -72,7 +72,6 @@ export class SessionInput extends React.PureComponent<Props, State> {
this.updateInputValue(e); this.updateInputValue(e);
}} }}
onKeyPress={event => { onKeyPress={event => {
event.persist();
if (event.key === 'Enter' && this.props.onEnterPressed) { if (event.key === 'Enter' && this.props.onEnterPressed) {
this.props.onEnterPressed(); this.props.onEnterPressed();
} }

View file

@ -285,14 +285,18 @@ class SessionPasswordModalInner extends React.Component<Props, State> {
if (event.key === 'Enter') { if (event.key === 'Enter') {
return this.setPassword(this.props.onOk); 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) { private async onPasswordConfirmInput(event: any) {
if (event.key === 'Enter') { if (event.key === 'Enter') {
return this.setPassword(this.props.onOk); return this.setPassword(this.props.onOk);
} }
this.setState({ currentPasswordConfirmEntered: event.target.value }); const currentPasswordConfirmEntered = event.target.value;
this.setState({ currentPasswordConfirmEntered });
} }
} }

View file

@ -444,7 +444,7 @@ export class SessionConversation extends React.Component<Props, State> {
isPrivate: conversation.isPrivate(), isPrivate: conversation.isPrivate(),
isPublic: conversation.isPublic(), isPublic: conversation.isPublic(),
isRss: conversation.isRss(), isRss: conversation.isRss(),
amMod: conversation.isModerator( isAdmin: conversation.isModerator(
window.storage.get('primaryDevicePubKey') window.storage.get('primaryDevicePubKey')
), ),
members, members,
@ -484,7 +484,7 @@ export class SessionConversation extends React.Component<Props, State> {
}, },
onUpdateGroupName: () => { onUpdateGroupName: () => {
conversation.onUpdateGroupName(); window.Whisper.events.trigger('updateGroupName', conversation);
}, },
onBlockUser: () => { onBlockUser: () => {
@ -549,12 +549,14 @@ export class SessionConversation extends React.Component<Props, State> {
const conversation = ConversationController.getInstance().getOrThrow( const conversation = ConversationController.getInstance().getOrThrow(
conversationKey conversationKey
); );
const ourPrimary = window.storage.get('primaryDevicePubKey');
const ourPK = window.textsecure.storage.user.getNumber();
const members = conversation.get('members') || []; const members = conversation.get('members') || [];
const isAdmin = conversation.isMediumGroup() const isAdmin = conversation.isMediumGroup()
? true ? true
: conversation.get('groupAdmins')?.includes(ourPK); : conversation.isPublic()
? conversation.isModerator(ourPrimary)
: false;
return { return {
id: conversation.id, id: conversation.id,
@ -563,7 +565,6 @@ export class SessionConversation extends React.Component<Props, State> {
phoneNumber: conversation.getNumber(), phoneNumber: conversation.getNumber(),
profileName: conversation.getProfileName(), profileName: conversation.getProfileName(),
avatarPath: conversation.getAvatarPath(), avatarPath: conversation.getAvatarPath(),
amMod: conversation.isModerator(),
isKickedFromGroup: conversation.get('isKickedFromGroup'), isKickedFromGroup: conversation.get('isKickedFromGroup'),
left: conversation.get('left'), left: conversation.get('left'),
isGroup: !conversation.isPrivate(), isGroup: !conversation.isPrivate(),
@ -601,7 +602,13 @@ export class SessionConversation extends React.Component<Props, State> {
onLeaveGroup: () => { onLeaveGroup: () => {
window.Whisper.events.trigger('leaveGroup', conversation); window.Whisper.events.trigger('leaveGroup', conversation);
}, },
onAddModerators: () => {
window.Whisper.events.trigger('addModerators', conversation);
},
onRemoveModerators: () => {
window.Whisper.events.trigger('removeModerators', conversation);
},
onShowLightBox: (lightBoxOptions = {}) => { onShowLightBox: (lightBoxOptions = {}) => {
this.setState({ lightBoxOptions }); this.setState({ lightBoxOptions });
}, },

View file

@ -29,7 +29,6 @@ interface Props {
timerOptions: Array<TimerOption>; timerOptions: Array<TimerOption>;
isPublic: boolean; isPublic: boolean;
isAdmin: boolean; isAdmin: boolean;
amMod: boolean;
isKickedFromGroup: boolean; isKickedFromGroup: boolean;
left: boolean; left: boolean;
isBlocked: boolean; isBlocked: boolean;
@ -40,6 +39,8 @@ interface Props {
onInviteContacts: () => void; onInviteContacts: () => void;
onLeaveGroup: () => void; onLeaveGroup: () => void;
onUpdateGroupName: () => void; onUpdateGroupName: () => void;
onAddModerators: () => void;
onRemoveModerators: () => void;
onUpdateGroupMembers: () => void; onUpdateGroupMembers: () => void;
onShowLightBox: (options: any) => void; onShowLightBox: (options: any) => void;
onSetDisappearingMessages: (seconds: number) => void; onSetDisappearingMessages: (seconds: number) => void;
@ -247,7 +248,6 @@ class SessionRightPanel extends React.Component<Props, State> {
left, left,
isPublic, isPublic,
isAdmin, isAdmin,
amMod,
isBlocked, isBlocked,
isGroup, isGroup,
} = this.props; } = this.props;
@ -273,10 +273,9 @@ class SessionRightPanel extends React.Component<Props, State> {
}; };
}); });
const showUpdateGroupNameButton = const showUpdateGroupNameButton = isAdmin && !commonNoShow;
isPublic && !commonNoShow const showAddRemoveModeratorsButton = isAdmin && !commonNoShow && isPublic;
? amMod && !commonNoShow
: isAdmin && !commonNoShow;
const showUpdateGroupMembersButton = !isPublic && !commonNoShow && isAdmin; const showUpdateGroupMembersButton = !isPublic && !commonNoShow && isAdmin;
return ( return (
@ -305,6 +304,25 @@ class SessionRightPanel extends React.Component<Props, State> {
{isPublic ? window.i18n('editGroup') : window.i18n('editGroupName')} {isPublic ? window.i18n('editGroup') : window.i18n('editGroupName')}
</div> </div>
)} )}
{showAddRemoveModeratorsButton && (
<>
<div
className="group-settings-item"
role="button"
onClick={this.props.onAddModerators}
>
{window.i18n('addModerators')}
</div>
<div
className="group-settings-item"
role="button"
onClick={this.props.onRemoveModerators}
>
{window.i18n('removeModerators')}
</div>
</>
)}
{showUpdateGroupMembersButton && ( {showUpdateGroupMembersButton && (
<div <div
className="group-settings-item" className="group-settings-item"

View file

@ -25,7 +25,7 @@ export type PropsConversationHeaderMenu = {
isKickedFromGroup?: boolean; isKickedFromGroup?: boolean;
left?: boolean; left?: boolean;
isGroup: boolean; isGroup: boolean;
amMod: boolean; isAdmin: boolean;
timerOptions: Array<TimerOption>; timerOptions: Array<TimerOption>;
isPrivate: boolean; isPrivate: boolean;
isBlocked: boolean; isBlocked: boolean;
@ -54,7 +54,7 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
isRss, isRss,
isGroup, isGroup,
isKickedFromGroup, isKickedFromGroup,
amMod, isAdmin,
timerOptions, timerOptions,
isBlocked, isBlocked,
isPrivate, isPrivate,
@ -115,19 +115,19 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
{getCopyMenuItem(isPublic, isRss, isGroup, onCopyPublicKey, window.i18n)} {getCopyMenuItem(isPublic, isRss, isGroup, onCopyPublicKey, window.i18n)}
{getDeleteMessagesMenuItem(isPublic, onDeleteMessages, window.i18n)} {getDeleteMessagesMenuItem(isPublic, onDeleteMessages, window.i18n)}
{getAddModeratorsMenuItem( {getAddModeratorsMenuItem(
amMod, isAdmin,
isKickedFromGroup, isKickedFromGroup,
onAddModerators, onAddModerators,
window.i18n window.i18n
)} )}
{getRemoveModeratorsMenuItem( {getRemoveModeratorsMenuItem(
amMod, isAdmin,
isKickedFromGroup, isKickedFromGroup,
onRemoveModerators, onRemoveModerators,
window.i18n window.i18n
)} )}
{getUpdateGroupNameMenuItem( {getUpdateGroupNameMenuItem(
amMod, isAdmin,
isKickedFromGroup, isKickedFromGroup,
left, left,
onUpdateGroupName, onUpdateGroupName,

View file

@ -75,25 +75,25 @@ function showDeleteContact(
} }
function showAddModerators( function showAddModerators(
amMod: boolean, isAdmin: boolean,
isKickedFromGroup: boolean isKickedFromGroup: boolean
): boolean { ): boolean {
return !isKickedFromGroup && amMod; return !isKickedFromGroup && isAdmin;
} }
function showRemoveModerators( function showRemoveModerators(
amMod: boolean, isAdmin: boolean,
isKickedFromGroup: boolean isKickedFromGroup: boolean
): boolean { ): boolean {
return !isKickedFromGroup && amMod; return !isKickedFromGroup && isAdmin;
} }
function showUpdateGroupName( function showUpdateGroupName(
amMod: boolean, isAdmin: boolean,
isKickedFromGroup: boolean, isKickedFromGroup: boolean,
left: boolean left: boolean
): boolean { ): boolean {
return !isKickedFromGroup && !left && amMod; return !isKickedFromGroup && !left && isAdmin;
} }
function showLeaveGroup( function showLeaveGroup(
@ -174,7 +174,7 @@ export function getLeaveGroupMenuItem(
} }
export function getUpdateGroupNameMenuItem( export function getUpdateGroupNameMenuItem(
amMod: boolean | undefined, isAdmin: boolean | undefined,
isKickedFromGroup: boolean | undefined, isKickedFromGroup: boolean | undefined,
left: boolean | undefined, left: boolean | undefined,
action: any, action: any,
@ -182,7 +182,7 @@ export function getUpdateGroupNameMenuItem(
): JSX.Element | null { ): JSX.Element | null {
if ( if (
showUpdateGroupName( showUpdateGroupName(
Boolean(amMod), Boolean(isAdmin),
Boolean(isKickedFromGroup), Boolean(isKickedFromGroup),
Boolean(left) Boolean(left)
) )
@ -193,24 +193,24 @@ export function getUpdateGroupNameMenuItem(
} }
export function getRemoveModeratorsMenuItem( export function getRemoveModeratorsMenuItem(
amMod: boolean | undefined, isAdmin: boolean | undefined,
isKickedFromGroup: boolean | undefined, isKickedFromGroup: boolean | undefined,
action: any, action: any,
i18n: LocalizerType i18n: LocalizerType
): JSX.Element | null { ): JSX.Element | null {
if (showRemoveModerators(Boolean(amMod), Boolean(isKickedFromGroup))) { if (showRemoveModerators(Boolean(isAdmin), Boolean(isKickedFromGroup))) {
return <Item onClick={action}>{i18n('removeModerators')}</Item>; return <Item onClick={action}>{i18n('removeModerators')}</Item>;
} }
return null; return null;
} }
export function getAddModeratorsMenuItem( export function getAddModeratorsMenuItem(
amMod: boolean | undefined, isAdmin: boolean | undefined,
isKickedFromGroup: boolean | undefined, isKickedFromGroup: boolean | undefined,
action: any, action: any,
i18n: LocalizerType i18n: LocalizerType
): JSX.Element | null { ): JSX.Element | null {
if (showAddModerators(Boolean(amMod), Boolean(isKickedFromGroup))) { if (showAddModerators(Boolean(isAdmin), Boolean(isKickedFromGroup))) {
return <Item onClick={action}>{i18n('addModerators')}</Item>; return <Item onClick={action}>{i18n('addModerators')}</Item>;
} }
return null; return null;

View file

@ -259,3 +259,11 @@ export function pushCannotRemoveCreatorFromGroup() {
window.i18n('cannotRemoveCreatorFromGroupDesc') window.i18n('cannotRemoveCreatorFromGroupDesc')
); );
} }
export function pushUserNeedsToHaveJoined() {
pushToastInfo(
'userNeedsToHaveJoined',
window.i18n('userNeedsToHaveJoined'),
window.i18n('userNeedsToHaveJoinedDesc')
);
}