You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/ts/components/menu/Menu.tsx

643 lines
18 KiB
TypeScript

import React, { useContext } from 'react';
import { Item, Submenu } from 'react-contexify';
import { useDispatch, useSelector } from 'react-redux';
import {
useAvatarPath,
useConversationUsername,
useHasNickname,
useIsActive,
useIsBlinded,
useIsBlocked,
useIsKickedFromGroup,
useIsLeft,
useIsMe,
useIsPrivate,
useIsPublic,
useIsRequest,
useNotificationSetting,
useWeAreAdmin,
} from '../../hooks/useParamSelector';
import {
approveConvoAndSendResponse,
blockConvoById,
clearNickNameByConvoId,
copyPublicKeyByConvoId,
declineConversationWithConfirm,
deleteAllMessagesByConvoIdWithConfirmation,
markAllReadByConvoId,
setDisappearingMessagesByConvoId,
setNotificationForConvoId,
showAddModeratorsByConvoId,
showBanUserByConvoId,
showInviteContactByConvoId,
showLeaveGroupByConvoId,
showRemoveModeratorsByConvoId,
showUnbanUserByConvoId,
showUpdateGroupNameByConvoId,
unblockConvoById,
} from '../../interactions/conversationInteractions';
import {
ConversationNotificationSetting,
ConversationNotificationSettingType,
} from '../../models/conversationAttributes';
import { getConversationController } from '../../session/conversations';
import {
changeNickNameModal,
updateConfirmModal,
updateUserDetailsModal,
} from '../../state/ducks/modalDialog';
import { SectionType } from '../../state/ducks/section';
import { hideMessageRequestBanner } from '../../state/ducks/userConfig';
import { getFocusedSection } from '../../state/selectors/section';
import { getTimerOptions } from '../../state/selectors/timerOptions';
import { LocalizerKeys } from '../../types/LocalizerKeys';
import { SessionButtonColor } from '../basic/SessionButton';
import { ContextConversationId } from '../leftpane/conversation-list-item/ConversationListItem';
function showTimerOptions(
isPublic: boolean,
isKickedFromGroup: boolean,
left: boolean,
isBlocked: boolean,
isRequest: boolean,
isActive: boolean
): boolean {
return !isPublic && !left && !isKickedFromGroup && !isBlocked && !isRequest && isActive;
}
function showNotificationConvo(
isKickedFromGroup: boolean,
left: boolean,
isBlocked: boolean,
isRequest: boolean
): boolean {
return !left && !isKickedFromGroup && !isBlocked && !isRequest;
}
function showBlock(isMe: boolean, isPrivate: boolean, isRequest: boolean): boolean {
return !isMe && isPrivate && !isRequest;
}
function showClearNickname(
isMe: boolean,
hasNickname: boolean,
isPrivate: boolean,
isRequest: boolean
): boolean {
return !isMe && hasNickname && isPrivate && !isRequest;
}
function showChangeNickname(isMe: boolean, isPrivate: boolean, isRequest: boolean) {
return !isMe && isPrivate && !isRequest;
}
// we want to show the copyId for open groups and private chats only
function showCopyId(isPublic: boolean, isPrivate: boolean, isBlinded: boolean): boolean {
return (isPrivate && !isBlinded) || isPublic;
}
function showDeleteContact(
isGroup: boolean,
isPublic: boolean,
isGroupLeft: boolean,
isKickedFromGroup: boolean,
isRequest: boolean
): boolean {
// you need to have left a closed group first to be able to delete it completely.
return (!isGroup && !isRequest) || (isGroup && (isGroupLeft || isKickedFromGroup || isPublic));
}
const showUnbanUser = (weAreAdmin: boolean, isPublic: boolean, isKickedFromGroup: boolean) => {
return !isKickedFromGroup && weAreAdmin && isPublic;
};
const showBanUser = (weAreAdmin: boolean, isPublic: boolean, isKickedFromGroup: boolean) => {
return !isKickedFromGroup && weAreAdmin && isPublic;
};
function showAddModerators(
weAreAdmin: boolean,
isPublic: boolean,
isKickedFromGroup: boolean
): boolean {
return !isKickedFromGroup && weAreAdmin && isPublic;
}
function showRemoveModerators(
weAreAdmin: boolean,
isPublic: boolean,
isKickedFromGroup: boolean
): boolean {
return !isKickedFromGroup && weAreAdmin && isPublic;
}
function showUpdateGroupName(
weAreAdmin: boolean,
isKickedFromGroup: boolean,
left: boolean
): boolean {
return !isKickedFromGroup && !left && weAreAdmin;
}
function showLeaveGroup(
isKickedFromGroup: boolean,
left: boolean,
isGroup: boolean,
isPublic: boolean
): boolean {
return !isKickedFromGroup && !left && isGroup && !isPublic;
}
function showInviteContact(isPublic: boolean): boolean {
return isPublic;
}
/** Menu items standardized */
export const InviteContactMenuItem = (): JSX.Element | null => {
const convoId = useContext(ContextConversationId);
const isPublic = useIsPublic(convoId);
if (showInviteContact(isPublic)) {
return (
<Item
onClick={() => {
showInviteContactByConvoId(convoId);
}}
>
{window.i18n('inviteContacts')}
</Item>
);
}
return null;
};
export const PinConversationMenuItem = (): JSX.Element | null => {
const conversationId = useContext(ContextConversationId);
const isMessagesSection = useSelector(getFocusedSection) === SectionType.Message;
const isRequest = useIsRequest(conversationId);
if (isMessagesSection && !isRequest) {
const conversation = getConversationController().get(conversationId);
const isPinned = conversation?.isPinned() || false;
const togglePinConversation = async () => {
await conversation?.setIsPinned(!isPinned);
};
const menuText = isPinned ? window.i18n('unpinConversation') : window.i18n('pinConversation');
return <Item onClick={togglePinConversation}>{menuText}</Item>;
}
return null;
};
export const DeleteContactMenuItem = () => {
const dispatch = useDispatch();
const convoId = useContext(ContextConversationId);
const isPublic = useIsPublic(convoId);
const isLeft = useIsLeft(convoId);
const isKickedFromGroup = useIsKickedFromGroup(convoId);
const isPrivate = useIsPrivate(convoId);
const isRequest = useIsRequest(convoId);
if (showDeleteContact(!isPrivate, isPublic, isLeft, isKickedFromGroup, isRequest)) {
let menuItemText: string;
if (isPublic) {
menuItemText = window.i18n('leaveGroup');
} else {
menuItemText = isPrivate
? window.i18n('editMenuDeleteContact')
: window.i18n('editMenuDeleteGroup');
}
const onClickClose = () => {
dispatch(updateConfirmModal(null));
};
const showConfirmationModal = () => {
dispatch(
updateConfirmModal({
title: menuItemText,
message: isPrivate
? window.i18n('deleteContactConfirmation')
: window.i18n('leaveGroupConfirmation'),
onClickClose,
okTheme: SessionButtonColor.Danger,
onClickOk: async () => {
await getConversationController().deleteContact(convoId);
},
})
);
};
return <Item onClick={showConfirmationModal}>{menuItemText}</Item>;
}
return null;
};
export const LeaveGroupMenuItem = () => {
const convoId = useContext(ContextConversationId);
const isPublic = useIsPublic(convoId);
const isLeft = useIsLeft(convoId);
const isKickedFromGroup = useIsKickedFromGroup(convoId);
const isPrivate = useIsPrivate(convoId);
if (showLeaveGroup(isKickedFromGroup, isLeft, !isPrivate, isPublic)) {
return (
<Item
onClick={() => {
showLeaveGroupByConvoId(convoId);
}}
>
{window.i18n('leaveGroup')}
</Item>
);
}
return null;
};
export const ShowUserDetailsMenuItem = () => {
const dispatch = useDispatch();
const convoId = useContext(ContextConversationId);
const isPrivate = useIsPrivate(convoId);
const avatarPath = useAvatarPath(convoId);
const userName = useConversationUsername(convoId) || convoId;
const isBlinded = useIsBlinded(convoId);
if (isPrivate && !isBlinded) {
return (
<Item
onClick={() => {
dispatch(
updateUserDetailsModal({
conversationId: convoId,
userName,
authorAvatarPath: avatarPath,
})
);
}}
>
{window.i18n('showUserDetails')}
</Item>
);
}
return null;
};
export const UpdateGroupNameMenuItem = () => {
const convoId = useContext(ContextConversationId);
const left = useIsLeft(convoId);
const isKickedFromGroup = useIsKickedFromGroup(convoId);
const weAreAdmin = useWeAreAdmin(convoId);
if (showUpdateGroupName(weAreAdmin, isKickedFromGroup, left)) {
return (
<Item
onClick={async () => {
await showUpdateGroupNameByConvoId(convoId);
}}
>
{window.i18n('editGroup')}
</Item>
);
}
return null;
};
export const RemoveModeratorsMenuItem = (): JSX.Element | null => {
const convoId = useContext(ContextConversationId);
const isPublic = useIsPublic(convoId);
const isKickedFromGroup = useIsKickedFromGroup(convoId);
const weAreAdmin = useWeAreAdmin(convoId);
if (showRemoveModerators(weAreAdmin, Boolean(isPublic), Boolean(isKickedFromGroup))) {
return (
<Item
onClick={() => {
showRemoveModeratorsByConvoId(convoId);
}}
>
{window.i18n('removeModerators')}
</Item>
);
}
return null;
};
export const AddModeratorsMenuItem = (): JSX.Element | null => {
const convoId = useContext(ContextConversationId);
const isPublic = useIsPublic(convoId);
const isKickedFromGroup = useIsKickedFromGroup(convoId);
const weAreAdmin = useWeAreAdmin(convoId);
if (showAddModerators(weAreAdmin, isPublic, isKickedFromGroup)) {
return (
<Item
onClick={() => {
showAddModeratorsByConvoId(convoId);
}}
>
{window.i18n('addModerators')}
</Item>
);
}
return null;
};
export const UnbanMenuItem = (): JSX.Element | null => {
const convoId = useContext(ContextConversationId);
const isPublic = useIsPublic(convoId);
const isKickedFromGroup = useIsKickedFromGroup(convoId);
const weAreAdmin = useWeAreAdmin(convoId);
if (showUnbanUser(weAreAdmin, isPublic, isKickedFromGroup)) {
return (
<Item
onClick={() => {
showUnbanUserByConvoId(convoId);
}}
>
{window.i18n('unbanUser')}
</Item>
);
}
return null;
};
export const BanMenuItem = (): JSX.Element | null => {
const convoId = useContext(ContextConversationId);
const isPublic = useIsPublic(convoId);
const isKickedFromGroup = useIsKickedFromGroup(convoId);
const weAreAdmin = useWeAreAdmin(convoId);
if (showBanUser(weAreAdmin, isPublic, isKickedFromGroup)) {
return (
<Item
onClick={() => {
showBanUserByConvoId(convoId);
}}
>
{window.i18n('banUser')}
</Item>
);
}
return null;
};
export const CopyMenuItem = (): JSX.Element | null => {
const convoId = useContext(ContextConversationId);
const isPublic = useIsPublic(convoId);
const isPrivate = useIsPrivate(convoId);
const isBlinded = useIsBlinded(convoId);
if (showCopyId(isPublic, isPrivate, isBlinded)) {
const copyIdLabel = isPublic ? window.i18n('copyOpenGroupURL') : window.i18n('copySessionID');
return <Item onClick={() => copyPublicKeyByConvoId(convoId)}>{copyIdLabel}</Item>;
}
return null;
};
export const MarkAllReadMenuItem = (): JSX.Element | null => {
const convoId = useContext(ContextConversationId);
const isRequest = useIsRequest(convoId);
if (!isRequest) {
return (
<Item onClick={() => markAllReadByConvoId(convoId)}>{window.i18n('markAllAsRead')}</Item>
);
} else {
return null;
}
};
export const DisappearingMessageMenuItem = (): JSX.Element | null => {
const convoId = useContext(ContextConversationId);
const isBlocked = useIsBlocked(convoId);
const isActive = useIsActive(convoId);
const isPublic = useIsPublic(convoId);
const isLeft = useIsLeft(convoId);
const isKickedFromGroup = useIsKickedFromGroup(convoId);
const timerOptions = useSelector(getTimerOptions).timerOptions;
const isRequest = useIsRequest(convoId);
if (
showTimerOptions(
Boolean(isPublic),
Boolean(isKickedFromGroup),
Boolean(isLeft),
Boolean(isBlocked),
isRequest,
isActive
)
) {
// const isRtlMode = isRtlBody();
return (
// Remove the && false to make context menu work with RTL support
<Submenu
label={window.i18n('disappearingMessages')}
// rtl={isRtlMode && false}
>
{timerOptions.map(item => (
<Item
key={item.value}
onClick={async () => {
await setDisappearingMessagesByConvoId(convoId, item.value);
}}
>
{item.name}
</Item>
))}
</Submenu>
);
}
return null;
};
export const NotificationForConvoMenuItem = (): JSX.Element | null => {
const convoId = useContext(ContextConversationId);
const isKickedFromGroup = useIsKickedFromGroup(convoId);
const left = useIsLeft(convoId);
const isBlocked = useIsBlocked(convoId);
const isPrivate = useIsPrivate(convoId);
const isRequest = useIsRequest(convoId);
const currentNotificationSetting = useNotificationSetting(convoId);
if (
showNotificationConvo(Boolean(isKickedFromGroup), Boolean(left), Boolean(isBlocked), isRequest)
) {
// const isRtlMode = isRtlBody();'
// exclude mentions_only settings for private chats as this does not make much sense
const notificationForConvoOptions = ConversationNotificationSetting.filter(n =>
isPrivate ? n !== 'mentions_only' : true
).map((n: ConversationNotificationSettingType) => {
// do this separately so typescript's compiler likes it
const keyToUse: LocalizerKeys =
n === 'all' || !n
? 'notificationForConvo_all'
: n === 'disabled'
? 'notificationForConvo_disabled'
: 'notificationForConvo_mentions_only';
return { value: n, name: window.i18n(keyToUse) };
});
return (
// Remove the && false to make context menu work with RTL support
<Submenu
label={window.i18n('notificationForConvo') as any}
// rtl={isRtlMode && false}
>
{(notificationForConvoOptions || []).map(item => {
const disabled = item.value === currentNotificationSetting;
return (
<Item
key={item.value}
onClick={async () => {
await setNotificationForConvoId(convoId, item.value);
}}
disabled={disabled}
>
{item.name}
</Item>
);
})}
</Submenu>
);
}
return null;
};
export function isRtlBody(): boolean {
const body = document.getElementsByTagName('body').item(0);
return body?.classList.contains('rtl') || false;
}
export const BlockMenuItem = (): JSX.Element | null => {
const convoId = useContext(ContextConversationId);
const isMe = useIsMe(convoId);
const isBlocked = useIsBlocked(convoId);
const isPrivate = useIsPrivate(convoId);
const isRequest = useIsRequest(convoId);
if (showBlock(Boolean(isMe), Boolean(isPrivate), Boolean(isRequest))) {
const blockTitle = isBlocked ? window.i18n('unblockUser') : window.i18n('blockUser');
const blockHandler = isBlocked
? () => unblockConvoById(convoId)
: () => blockConvoById(convoId);
return <Item onClick={blockHandler}>{blockTitle}</Item>;
}
return null;
};
export const ClearNicknameMenuItem = (): JSX.Element | null => {
const convoId = useContext(ContextConversationId);
const isMe = useIsMe(convoId);
const hasNickname = useHasNickname(convoId);
const isPrivate = useIsPrivate(convoId);
const isRequest = Boolean(useIsRequest(convoId)); // easier to copy paste
if (showClearNickname(Boolean(isMe), Boolean(hasNickname), Boolean(isPrivate), isRequest)) {
return (
<Item onClick={() => clearNickNameByConvoId(convoId)}>{window.i18n('clearNickname')}</Item>
);
}
return null;
};
export const ChangeNicknameMenuItem = () => {
const convoId = useContext(ContextConversationId);
const isMe = useIsMe(convoId);
const isPrivate = useIsPrivate(convoId);
const isRequest = useIsRequest(convoId);
const dispatch = useDispatch();
if (showChangeNickname(isMe, isPrivate, isRequest)) {
return (
<Item
onClick={() => {
dispatch(changeNickNameModal({ conversationId: convoId }));
}}
>
{window.i18n('changeNickname')}
</Item>
);
}
return null;
};
export const DeleteMessagesMenuItem = () => {
const convoId = useContext(ContextConversationId);
const isRequest = useIsRequest(convoId);
if (isRequest) {
return null;
}
return (
<Item
onClick={() => {
deleteAllMessagesByConvoIdWithConfirmation(convoId);
}}
>
{window.i18n('deleteMessages')}
</Item>
);
};
export const HideBannerMenuItem = (): JSX.Element => {
const dispatch = useDispatch();
return (
<Item
onClick={() => {
dispatch(hideMessageRequestBanner());
}}
>
{window.i18n('hideBanner')}
</Item>
);
};
export const AcceptMenuItem = () => {
const convoId = useContext(ContextConversationId);
const isRequest = useIsRequest(convoId);
const convo = getConversationController().get(convoId);
if (isRequest) {
return (
<Item
onClick={async () => {
await convo.setDidApproveMe(true);
await convo.addOutgoingApprovalMessage(Date.now());
await approveConvoAndSendResponse(convoId, true);
}}
>
{window.i18n('accept')}
</Item>
);
}
return null;
};
export const DeclineMenuItem = () => {
const convoId = useContext(ContextConversationId);
const isRequest = useIsRequest(convoId);
if (isRequest) {
return (
<Item
onClick={() => {
declineConversationWithConfirm(convoId, true);
}}
>
{window.i18n('decline')}
</Item>
);
}
return null;
};