* adding basic functionaliy for unbanning a user

* merge ban and unban user dialog in to one dialog

Co-authored-by: warrickct <warrickct@gmail.com>
This commit is contained in:
Audric Ackermann 2021-12-16 15:04:26 +11:00 committed by GitHub
parent e17b5e0671
commit 38325215e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 286 additions and 111 deletions

View File

@ -252,9 +252,7 @@
"userUnbanned": "User unbanned successfully",
"userUnbanFailed": "Unban failed!",
"banUser": "Ban User",
"banUserConfirm": "Are you sure you want to ban the user?",
"banUserAndDeleteAll": "Ban and Delete All",
"banUserAndDeleteAllConfirm": "Are you sure you want to ban the user and delete all his messages?",
"userBanned": "Banned successfully",
"userBanFailed": "Ban failed!",
"leaveGroup": "Leave Group",
@ -463,5 +461,6 @@
"callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.",
"menuCall": "Call",
"startedACall": "You called $name$",
"answeredACall": "Call with $name$"
"answeredACall": "Call with $name$",
"banAndDeleteAllDialogTitle": "Ban and Delete All Messages Confirmation"
}

View File

@ -106,6 +106,7 @@ export const SessionWrapperModal = (props: SessionWrapperModalType) => {
<div className="session-modal__body">
<div className="session-modal__centered">
{props.children}
<div className="session-modal__button-group">
{onConfirm ? (
<SessionButton onClick={props.onConfirm}>

View File

@ -7,16 +7,12 @@ type Props = {
export const SessionSpinner = (props: Props) => {
const { loading } = props;
return (
<>
{loading ? (
<div className="session-loader">
<div />
<div />
<div />
<div />
</div>
) : null}
</>
);
return loading ? (
<div className="session-loader">
<div />
<div />
<div />
<div />
</div>
) : null;
};

View File

@ -150,10 +150,6 @@ export const MessageContextMenu = (props: Props) => {
MessageInteraction.banUser(authorPhoneNumber, convoId);
}, [authorPhoneNumber, convoId]);
const onBanAndDeleteAll = useCallback(() => {
MessageInteraction.banUser(authorPhoneNumber, convoId, true);
}, [authorPhoneNumber, convoId]);
const onUnban = useCallback(() => {
MessageInteraction.unbanUser(authorPhoneNumber, convoId);
}, [authorPhoneNumber, convoId]);
@ -203,9 +199,6 @@ export const MessageContextMenu = (props: Props) => {
</>
) : null}
{weAreAdmin && isPublic ? <Item onClick={onBan}>{window.i18n('banUser')}</Item> : null}
{weAreAdmin && isPublic ? (
<Item onClick={onBanAndDeleteAll}>{window.i18n('banUserAndDeleteAll')}</Item>
) : null}
{weAreAdmin && isOpenGroupV2 ? (
<Item onClick={onUnban}>{window.i18n('unbanUser')}</Item>
) : null}

View File

@ -54,8 +54,6 @@ const ChangeItemLeft = (left: Array<string>): string => {
const ChangeItem = (change: PropsForGroupUpdateType): string => {
switch (change.type) {
case 'name':
console.warn('name: ', change.newName);
return window.i18n('titleIsNow', [change.newName || '']);
case 'add':
return ChangeItemJoined(change.added);

View File

@ -0,0 +1,160 @@
import React, { useRef, useState } from 'react';
import { PubKey } from '../../session/types';
import { ToastUtils } from '../../session/utils';
import { Flex } from '../basic/Flex';
import { useDispatch } from 'react-redux';
import { BanType, updateBanOrUnbanUserModal } from '../../state/ducks/modalDialog';
import { SpacerSM } from '../basic/Text';
import { getConversationController } from '../../session/conversations/ConversationController';
import { ApiV2 } from '../../session/apis/open_group_api/opengroupV2';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { SessionSpinner } from '../basic/SessionSpinner';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { ConversationModel } from '../../models/conversation';
import { useFocusMount } from '../../hooks/useFocusMount';
import { useConversationPropsById } from '../../hooks/useParamSelector';
// tslint:disable: use-simple-attributes
async function banOrUnBanUserCall(
convo: ConversationModel,
textValue: string,
banType: BanType,
deleteAll: boolean
) {
// if we don't have valid data entered by the user
const pubkey = PubKey.from(textValue);
if (!pubkey) {
window.log.info(`invalid pubkey for ${banType} user:${textValue}`);
ToastUtils.pushInvalidPubKey();
return false;
}
try {
// this is a v2 opengroup
const roomInfos = convo.toOpenGroupV2();
const isChangeApplied =
banType === 'ban'
? await ApiV2.banUser(pubkey, roomInfos, deleteAll)
: await ApiV2.unbanUser(pubkey, roomInfos);
if (!isChangeApplied) {
window?.log?.warn(`failed to ${banType} user: ${isChangeApplied}`);
banType === 'ban' ? ToastUtils.pushUserBanFailure() : ToastUtils.pushUserUnbanSuccess();
return false;
}
window?.log?.info(`${pubkey.key} user ${banType}ned successfully...`);
banType === 'ban' ? ToastUtils.pushUserBanSuccess() : ToastUtils.pushUserUnbanSuccess();
return true;
} catch (e) {
window?.log?.error(`Got error while ${banType}ning user:`, e);
return false;
}
}
export const BanOrUnBanUserDialog = (props: {
conversationId: string;
banType: BanType;
pubkey?: string;
}) => {
const { conversationId, banType, pubkey } = props;
const { i18n } = window;
const isBan = banType === 'ban';
const dispatch = useDispatch();
const convo = getConversationController().get(conversationId);
const inputRef = useRef(null);
useFocusMount(inputRef, true);
const wasGivenAPubkey = Boolean(pubkey?.length);
const [inputBoxValue, setInputBoxValue] = useState('');
const [inProgress, setInProgress] = useState(false);
const sourceConvoProps = useConversationPropsById(pubkey);
const inputTextToDisplay =
wasGivenAPubkey && sourceConvoProps
? `${sourceConvoProps.profileName} ${PubKey.shorten(sourceConvoProps.id)}`
: undefined;
/**
* Ban or Unban a user from an open group
* @param deleteAll Delete all messages for that user in the group (only works with ban)
*/
const banOrUnBanUser = async (deleteAll: boolean = false) => {
const castedPubkey = pubkey?.length ? pubkey : inputBoxValue;
window?.log?.info(`asked to ${banType} user: ${castedPubkey}, banAndDeleteAll:${deleteAll}`);
setInProgress(true);
const isBanned = await banOrUnBanUserCall(convo, castedPubkey, banType, deleteAll);
if (isBanned) {
// clear input box
setInputBoxValue('');
if (wasGivenAPubkey) {
dispatch(updateBanOrUnbanUserModal(null));
}
}
setInProgress(false);
};
const chatName = convo.get('name');
const title = `${isBan ? window.i18n('banUser') : window.i18n('unbanUser')}: ${chatName}`;
const onPubkeyBoxChanges = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputBoxValue(e.target.value?.trim() || '');
};
/**
* Starts procedure for banning/unbanning user and all their messages using dialog
*/
const startBanAndDeleteAllSequence = async () => {
await banOrUnBanUser(true);
};
const buttonText = isBan ? i18n('banUser') : i18n('unbanUser');
return (
<SessionWrapperModal
showExitIcon={true}
title={title}
onClose={() => {
dispatch(updateBanOrUnbanUserModal(null));
}}
>
<Flex container={true} flexDirection="column" alignItems="center">
<input
ref={inputRef}
type="text"
className="module-main-header__search__input"
placeholder={i18n('enterSessionID')}
dir="auto"
onChange={onPubkeyBoxChanges}
disabled={inProgress || wasGivenAPubkey}
value={wasGivenAPubkey ? inputTextToDisplay : inputBoxValue}
/>
<Flex container={true}>
<SessionButton
buttonType={SessionButtonType.Square}
buttonColor={SessionButtonColor.Primary}
onClick={banOrUnBanUser}
text={buttonText}
disabled={inProgress}
/>
{isBan && (
<>
<SpacerSM />
<SessionButton
buttonType={SessionButtonType.Square}
buttonColor={SessionButtonColor.Danger}
onClick={startBanAndDeleteAllSequence}
text={i18n('banUserAndDeleteAll')}
disabled={inProgress}
/>
</>
)}
</Flex>
<SessionSpinner loading={inProgress} />
</Flex>
</SessionWrapperModal>
);
};

View File

@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
import {
getAddModeratorsModal,
getAdminLeaveClosedGroupDialog,
getBanOrUnbanUserModalState,
getChangeNickNameDialog,
getConfirmModal,
getDeleteAccountModalState,
@ -30,6 +31,7 @@ import { RemoveModeratorsDialog } from './ModeratorsRemoveDialog';
import { UpdateGroupMembersDialog } from './UpdateGroupMembersDialog';
import { UpdateGroupNameDialog } from './UpdateGroupNameDialog';
import { SessionNicknameDialog } from './SessionNicknameDialog';
import { BanOrUnBanUserDialog } from './BanOrUnbanUserDialog';
export const ModalContainer = () => {
const confirmModalState = useSelector(getConfirmModal);
@ -46,9 +48,11 @@ export const ModalContainer = () => {
const adminLeaveClosedGroupModalState = useSelector(getAdminLeaveClosedGroupDialog);
const sessionPasswordModalState = useSelector(getSessionPasswordDialog);
const deleteAccountModalState = useSelector(getDeleteAccountModalState);
const banOrUnbanUserModalState = useSelector(getBanOrUnbanUserModalState);
return (
<>
{banOrUnbanUserModalState && <BanOrUnBanUserDialog {...banOrUnbanUserModalState} />}
{inviteModalState && <InviteContactsDialog {...inviteModalState} />}
{addModeratorsModalState && <AddModeratorsDialog {...addModeratorsModalState} />}
{removeModeratorsModalState && <RemoveModeratorsDialog {...removeModeratorsModalState} />}

View File

@ -315,6 +315,7 @@ const ConversationListItem = (props: Props) => {
avatarPath,
isPrivate,
currentNotificationSetting,
weAreAdmin,
isMessageRequest,
} = props;
const triggerId = `conversation-item-${conversationId}-ctxmenu`;
@ -389,6 +390,7 @@ const ConversationListItem = (props: Props) => {
type={type}
currentNotificationSetting={currentNotificationSetting || 'all'}
avatarPath={avatarPath || null}
weAreAdmin={weAreAdmin}
/>
</Portal>
</div>

View File

@ -2,6 +2,7 @@ import React from 'react';
import { animation, Menu } from 'react-contexify';
import {
getAddModeratorsMenuItem,
getBanMenuItem,
getBlockMenuItem,
getChangeNicknameMenuItem,
getClearNicknameMenuItem,
@ -16,6 +17,7 @@ import {
getPinConversationMenuItem,
getRemoveModeratorsMenuItem,
getShowUserDetailsMenuItem,
getUnbanMenuItem,
getUpdateGroupNameMenuItem,
} from './Menu';
import _ from 'lodash';
@ -79,6 +81,8 @@ const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
{getDeleteMessagesMenuItem(conversationId)}
{getAddModeratorsMenuItem(weAreAdmin, isPublic, isKickedFromGroup, conversationId)}
{getRemoveModeratorsMenuItem(weAreAdmin, isPublic, isKickedFromGroup, conversationId)}
{getBanMenuItem(weAreAdmin, isPublic, isKickedFromGroup, conversationId)}
{getUnbanMenuItem(weAreAdmin, isPublic, isKickedFromGroup, conversationId)}
{getUpdateGroupNameMenuItem(weAreAdmin, isKickedFromGroup, left, conversationId)}
{getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, conversationId)}
{getInviteContactMenuItem(isGroup, isPublic, conversationId)}

View File

@ -8,6 +8,7 @@ import {
} from '../../models/conversation';
import {
getBanMenuItem,
getBlockMenuItem,
getChangeNicknameMenuItem,
getClearNicknameMenuItem,
@ -20,6 +21,7 @@ import {
getNotificationForConvoMenuItem,
getPinConversationMenuItem,
getShowUserDetailsMenuItem,
getUnbanMenuItem,
} from './Menu';
export type PropsContextConversationItem = {
@ -36,6 +38,7 @@ export type PropsContextConversationItem = {
theme?: any;
currentNotificationSetting: ConversationNotificationSettingType;
avatarPath: string | null;
weAreAdmin?: boolean;
};
const ConversationListItemContextMenu = (props: PropsContextConversationItem) => {
@ -51,6 +54,7 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) =>
isKickedFromGroup,
currentNotificationSetting,
isPrivate,
weAreAdmin,
} = props;
const isGroup = type === 'group';
@ -75,6 +79,8 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) =>
{getChangeNicknameMenuItem(isMe, isGroup, conversationId)}
{getClearNicknameMenuItem(isMe, hasNickname, isGroup, conversationId)}
{getDeleteMessagesMenuItem(conversationId)}
{getBanMenuItem(weAreAdmin, isPublic, isKickedFromGroup, conversationId)}
{getUnbanMenuItem(weAreAdmin, isPublic, isKickedFromGroup, conversationId)}
{getInviteContactMenuItem(isGroup, isPublic, conversationId)}
{getDeleteContactMenuItem(isGroup, isPublic, left, isKickedFromGroup, conversationId)}
{getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, conversationId)}

View File

@ -11,9 +11,11 @@ import {
setDisappearingMessagesByConvoId,
setNotificationForConvoId,
showAddModeratorsByConvoId,
showBanUserByConvoId,
showInviteContactByConvoId,
showLeaveGroupByConvoId,
showRemoveModeratorsByConvoId,
showUnbanUserByConvoId,
showUpdateGroupNameByConvoId,
unblockConvoById,
} from '../../interactions/conversationInteractions';
@ -81,6 +83,14 @@ function showDeleteContact(
return !isGroup || (isGroup && (isGroupLeft || isKickedFromGroup || isPublic));
}
const showUnbanUser = (isAdmin: boolean, isPublic: boolean, isKickedFromGroup: boolean) => {
return !isKickedFromGroup && isAdmin && isPublic;
};
const showBanUser = (isAdmin: boolean, isPublic: boolean, isKickedFromGroup: boolean) => {
return !isKickedFromGroup && isAdmin && isPublic;
};
function showAddModerators(
isAdmin: boolean,
isPublic: boolean,
@ -327,6 +337,47 @@ export function getAddModeratorsMenuItem(
return null;
}
export function getUnbanMenuItem(
isAdmin: boolean | undefined,
isPublic: boolean | undefined,
isKickedFromGroup: boolean | undefined,
conversationId: string
): JSX.Element | null {
if (showUnbanUser(Boolean(isAdmin), Boolean(isPublic), Boolean(isKickedFromGroup))) {
return (
<Item
onClick={() => {
showUnbanUserByConvoId(conversationId);
}}
>
{window.i18n('unbanUser')}
</Item>
);
}
// TODO: translations
return null;
}
export function getBanMenuItem(
isAdmin: boolean | undefined,
isPublic: boolean | undefined,
isKickedFromGroup: boolean | undefined,
conversationId: string
): JSX.Element | null {
if (showBanUser(Boolean(isAdmin), Boolean(isPublic), Boolean(isKickedFromGroup))) {
return (
<Item
onClick={() => {
showBanUserByConvoId(conversationId);
}}
>
{window.i18n('banUser')}
</Item>
);
}
return null;
}
export function getCopyMenuItem(
isPublic: boolean | undefined,
isGroup: boolean | undefined,

View File

@ -14,6 +14,7 @@ import {
adminLeaveClosedGroup,
changeNickNameModal,
updateAddModeratorsModal,
updateBanOrUnbanUserModal,
updateConfirmModal,
updateGroupMembersModal,
updateGroupNameModal,
@ -213,6 +214,18 @@ export function showRemoveModeratorsByConvoId(conversationId: string) {
window.inboxStore?.dispatch(updateRemoveModeratorsModal({ conversationId }));
}
export function showBanUserByConvoId(conversationId: string, pubkey?: string) {
window.inboxStore?.dispatch(
updateBanOrUnbanUserModal({ banType: 'ban', conversationId, pubkey })
);
}
export function showUnbanUserByConvoId(conversationId: string, pubkey?: string) {
window.inboxStore?.dispatch(
updateBanOrUnbanUserModal({ banType: 'unban', conversationId, pubkey })
);
}
export async function markAllReadByConvoId(conversationId: string) {
const conversation = getConversationController().get(conversationId);
perfStart(`markAllReadByConvoId-${conversationId}`);

View File

@ -1,5 +1,4 @@
import _ from 'lodash';
import { getV2OpenGroupRoom } from '../data/opengroups';
import { ApiV2 } from '../session/apis/open_group_api/opengroupV2';
import { joinOpenGroupV2WithUIEvents } from '../session/apis/open_group_api/opengroupV2/JoinOpenGroupV2';
import {
@ -10,13 +9,9 @@ import { getConversationController } from '../session/conversations';
import { PubKey } from '../session/types';
import { ToastUtils } from '../session/utils';
import { updateConfirmModal } from '../state/ducks/modalDialog';
import { updateBanOrUnbanUserModal, updateConfirmModal } from '../state/ducks/modalDialog';
export function banUser(
userToBan: string,
conversationId: string,
deleteAllMessages: boolean = false
) {
export function banUser(userToBan: string, conversationId: string) {
let pubKeyToBan: PubKey;
try {
pubKeyToBan = PubKey.cast(userToBan);
@ -25,50 +20,16 @@ export function banUser(
ToastUtils.pushUserBanFailure();
return;
}
if (!isOpenGroupV2(conversationId)) {
window.log.warn(`Conversation ${conversationId} is not an open group`);
ToastUtils.pushUserBanFailure();
const onClickClose = () => {
window.inboxStore?.dispatch(updateConfirmModal(null));
};
return;
}
const title = deleteAllMessages ? window.i18n('banUserAndDeleteAll') : window.i18n('banUser');
const message = deleteAllMessages
? window.i18n('banUserAndDeleteAllConfirm')
: window.i18n('banUserConfirm');
const confirmationModalProps = {
title,
message,
onClickClose,
onClickOk: async () => {
const conversation = getConversationController().get(conversationId);
if (!conversation) {
window.log.info('cannot ban user, the corresponding conversation was not found.');
return;
}
let success = false;
if (isOpenGroupV2(conversation.id)) {
const roomInfos = await getV2OpenGroupRoom(conversation.id);
if (!roomInfos) {
window.log.warn('banUser room not found');
} else {
success = await ApiV2.banUser(
pubKeyToBan,
_.pick(roomInfos, 'serverUrl', 'roomId'),
deleteAllMessages
);
}
} else {
throw new Error('V1 opengroup are not supported');
}
if (success) {
ToastUtils.pushUserBanSuccess();
} else {
ToastUtils.pushUserBanFailure();
}
},
};
window.inboxStore?.dispatch(updateConfirmModal(confirmationModalProps));
window.inboxStore?.dispatch(
updateBanOrUnbanUserModal({ banType: 'ban', conversationId, pubkey: pubKeyToBan.key })
);
}
/**
@ -84,45 +45,14 @@ export function unbanUser(userToUnBan: string, conversationId: string) {
ToastUtils.pushUserBanFailure();
return;
}
if (!isOpenGroupV2(conversationId || '')) {
window?.log?.warn('no way to unban on a opengroupv1');
ToastUtils.pushUserBanFailure();
if (!isOpenGroupV2(conversationId)) {
window.log.warn(`Conversation ${conversationId} is not an open group`);
ToastUtils.pushUserUnbanFailure();
return;
}
const onClickClose = () => window.inboxStore?.dispatch(updateConfirmModal(null));
const onClickOk = async () => {
const conversation = getConversationController().get(conversationId);
if (!conversation) {
// double check here. the convo might have been removed since the dialog was opened
window.log.info('cannot unban user, the corresponding conversation was not found.');
return;
}
let success = false;
if (isOpenGroupV2(conversation.id)) {
const roomInfos = await getV2OpenGroupRoom(conversation.id);
if (!roomInfos) {
window.log.warn('unbanUser room not found');
} else {
success = await ApiV2.unbanUser(pubKeyToUnban, _.pick(roomInfos, 'serverUrl', 'roomId'));
}
}
if (success) {
ToastUtils.pushUserUnbanSuccess();
} else {
ToastUtils.pushUserUnbanFailure();
}
};
window.inboxStore?.dispatch(
updateConfirmModal({
title: window.i18n('unbanUser'),
message: window.i18n('unbanUserConfirm'),
onClickOk,
onClickClose,
})
updateBanOrUnbanUserModal({ banType: 'unban', conversationId, pubkey: pubKeyToUnban.key })
);
}

View File

@ -1,9 +1,15 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { SessionConfirmDialogProps } from '../../components/dialog/SessionConfirm';
import { PasswordAction } from '../../components/dialog/SessionPasswordDialog';
export type BanType = 'ban' | 'unban';
export type ConfirmModalState = SessionConfirmDialogProps | null;
export type InviteContactModalState = { conversationId: string } | null;
export type BanOrUnbanUserModalState = {
conversationId: string;
banType: BanType;
pubkey?: string;
} | null;
export type AddModeratorsModalState = InviteContactModalState;
export type RemoveModeratorsModalState = InviteContactModalState;
export type UpdateGroupMembersModalState = InviteContactModalState;
@ -26,8 +32,9 @@ export type UserDetailsModalState = {
export type ModalState = {
confirmModal: ConfirmModalState;
inviteContactModal: InviteContactModalState;
addModeratorsModal: AddModeratorsModalState;
banOrUnbanUserModal: BanOrUnbanUserModalState;
removeModeratorsModal: RemoveModeratorsModalState;
addModeratorsModal: AddModeratorsModalState;
groupNameModal: UpdateGroupNameModalState;
groupMembersModal: UpdateGroupMembersModalState;
userDetailsModal: UserDetailsModalState;
@ -45,6 +52,7 @@ export const initialModalState: ModalState = {
inviteContactModal: null,
addModeratorsModal: null,
removeModeratorsModal: null,
banOrUnbanUserModal: null,
groupNameModal: null,
groupMembersModal: null,
userDetailsModal: null,
@ -67,6 +75,9 @@ const ModalSlice = createSlice({
updateInviteContactModal(state, action: PayloadAction<InviteContactModalState | null>) {
return { ...state, inviteContactModal: action.payload };
},
updateBanOrUnbanUserModal(state, action: PayloadAction<BanOrUnbanUserModalState | null>) {
return { ...state, banOrUnbanUserModal: action.payload };
},
updateAddModeratorsModal(state, action: PayloadAction<AddModeratorsModalState | null>) {
return { ...state, addModeratorsModal: action.payload };
},
@ -122,5 +133,6 @@ export const {
adminLeaveClosedGroup,
sessionPassword,
updateDeleteAccountModal,
updateBanOrUnbanUserModal,
} = actions;
export const modalReducer = reducer;

View File

@ -4,6 +4,7 @@ import { StateType } from '../reducer';
import {
AddModeratorsModalState,
AdminLeaveClosedGroupModalState,
BanOrUnbanUserModalState,
ChangeNickNameModalState,
ConfirmModalState,
DeleteAccountModalState,
@ -43,6 +44,11 @@ export const getRemoveModeratorsModal = createSelector(
(state: ModalState): RemoveModeratorsModalState => state.removeModeratorsModal
);
export const getBanOrUnbanUserModalState = createSelector(
getModal,
(state: ModalState): BanOrUnbanUserModalState => state.banOrUnbanUserModal
);
export const getUpdateGroupNameModal = createSelector(
getModal,
(state: ModalState): UpdateGroupNameModalState => state.groupNameModal

View File

@ -373,7 +373,7 @@ export type LocalizerKeys =
| 'password'
| 'usersCanShareTheir...'
| 'timestampFormat_M'
| 'banUserAndDeleteAllConfirm'
| 'banAndDeleteAllDialogTitle'
| 'nicknamePlaceholder'
| 'linkPreviewsTitle'
| 'continue'