import React from 'react'; import { getHasIncomingCall, getHasOngoingCall } from '../../../state/selectors/call'; import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations'; import { getFocusedSection } from '../../../state/selectors/section'; import { Item, Submenu } from 'react-contexify'; import { ConversationNotificationSetting, ConversationNotificationSettingType, } from '../../../models/conversation'; import { useDispatch, useSelector } from 'react-redux'; import { changeNickNameModal, updateConfirmModal, updateUserDetailsModal, } from '../../../state/ducks/modalDialog'; import { SectionType } from '../../../state/ducks/section'; import { getConversationController } from '../../../session/conversations'; import { blockConvoById, callRecipient, clearNickNameByConvoId, copyPublicKeyByConvoId, deleteAllMessagesByConvoIdWithConfirmation, markAllReadByConvoId, setDisappearingMessagesByConvoId, setNotificationForConvoId, showAddModeratorsByConvoId, showInviteContactByConvoId, showLeaveGroupByConvoId, showRemoveModeratorsByConvoId, showUpdateGroupNameByConvoId, unblockConvoById, } from '../../../interactions/conversationInteractions'; import { SessionButtonColor } from '../SessionButton'; import { getTimerOptions } from '../../../state/selectors/timerOptions'; import { ToastUtils } from '../../../session/utils'; import { LocalizerKeys } from '../../../types/LocalizerKeys'; const maxNumberOfPinnedConversations = 5; function showTimerOptions( isPublic: boolean, isKickedFromGroup: boolean, left: boolean, isBlocked: boolean ): boolean { return !isPublic && !left && !isKickedFromGroup && !isBlocked; } function showNotificationConvo( isKickedFromGroup: boolean, left: boolean, isBlocked: boolean ): boolean { return !left && !isKickedFromGroup && !isBlocked; } function showBlock(isMe: boolean, isPrivate: boolean): boolean { return !isMe && isPrivate; } function showClearNickname(isMe: boolean, hasNickname: boolean, isGroup: boolean): boolean { return !isMe && hasNickname && !isGroup; } function showChangeNickname(isMe: boolean, isGroup: boolean) { return !isMe && !isGroup; } // we want to show the copyId for open groups and private chats only function showCopyId(isPublic: boolean, isGroup: boolean): boolean { return !isGroup || isPublic; } function showDeleteContact( isGroup: boolean, isPublic: boolean, isGroupLeft: boolean, isKickedFromGroup: boolean ): boolean { // you need to have left a closed group first to be able to delete it completely. return !isGroup || (isGroup && (isGroupLeft || isKickedFromGroup || isPublic)); } function showAddModerators( isAdmin: boolean, isPublic: boolean, isKickedFromGroup: boolean ): boolean { return !isKickedFromGroup && isAdmin && isPublic; } function showRemoveModerators( isAdmin: boolean, isPublic: boolean, isKickedFromGroup: boolean ): boolean { return !isKickedFromGroup && isAdmin && isPublic; } function showUpdateGroupName(isAdmin: boolean, isKickedFromGroup: boolean, left: boolean): boolean { return !isKickedFromGroup && !left && isAdmin; } function showLeaveGroup( isKickedFromGroup: boolean, left: boolean, isGroup: boolean, isPublic: boolean ): boolean { return !isKickedFromGroup && !left && isGroup && !isPublic; } function showInviteContact(isGroup: boolean, isPublic: boolean): boolean { return isGroup && isPublic; } /** Menu items standardized */ export function getInviteContactMenuItem( isGroup: boolean | undefined, isPublic: boolean | undefined, conversationId: string ): JSX.Element | null { if (showInviteContact(Boolean(isGroup), Boolean(isPublic))) { return ( { showInviteContactByConvoId(conversationId); }} > {window.i18n('inviteContacts')} ); } return null; } export interface PinConversationMenuItemProps { conversationId: string; } export const getPinConversationMenuItem = (conversationId: string): JSX.Element | null => { const isMessagesSection = useSelector(getFocusedSection) === SectionType.Message; const nbOfAlreadyPinnedConvos = useSelector(getNumberOfPinnedConversations); if (isMessagesSection) { const conversation = getConversationController().get(conversationId); const isPinned = conversation.isPinned(); const togglePinConversation = async () => { if ((!isPinned && nbOfAlreadyPinnedConvos < maxNumberOfPinnedConversations) || isPinned) { await conversation.setIsPinned(!isPinned); } else { ToastUtils.pushToastWarning( 'pinConversationLimitToast', window.i18n('pinConversationLimitTitle'), window.i18n('pinConversationLimitToastDescription', [`${maxNumberOfPinnedConversations}`]) ); } }; const menuText = isPinned ? window.i18n('unpinConversation') : window.i18n('pinConversation'); return {menuText}; } return null; }; export function getDeleteContactMenuItem( isGroup: boolean | undefined, isPublic: boolean | undefined, isLeft: boolean | undefined, isKickedFromGroup: boolean | undefined, conversationId: string ): JSX.Element | null { const dispatch = useDispatch(); if ( showDeleteContact( Boolean(isGroup), Boolean(isPublic), Boolean(isLeft), Boolean(isKickedFromGroup) ) ) { let menuItemText: string; if (isPublic) { menuItemText = window.i18n('leaveGroup'); } else { menuItemText = window.i18n('delete'); } const onClickClose = () => { dispatch(updateConfirmModal(null)); }; const showConfirmationModal = () => { dispatch( updateConfirmModal({ title: menuItemText, message: isGroup ? window.i18n('leaveGroupConfirmation') : window.i18n('deleteContactConfirmation'), onClickClose, okTheme: SessionButtonColor.Danger, onClickOk: async () => { await getConversationController().deleteContact(conversationId); }, }) ); }; return {menuItemText}; } return null; } export function getLeaveGroupMenuItem( isKickedFromGroup: boolean | undefined, left: boolean | undefined, isGroup: boolean | undefined, isPublic: boolean | undefined, conversationId: string ): JSX.Element | null { if ( showLeaveGroup(Boolean(isKickedFromGroup), Boolean(left), Boolean(isGroup), Boolean(isPublic)) ) { return ( { showLeaveGroupByConvoId(conversationId); }} > {window.i18n('leaveGroup')} ); } return null; } export function getShowUserDetailsMenuItem( isPrivate: boolean | undefined, conversationId: string, avatarPath: string | null, userName: string ): JSX.Element | null { const dispatch = useDispatch(); if (isPrivate) { return ( { dispatch( updateUserDetailsModal({ conversationId: conversationId, userName, authorAvatarPath: avatarPath, }) ); }} > {window.i18n('showUserDetails')} ); } return null; } export function getUpdateGroupNameMenuItem( isAdmin: boolean | undefined, isKickedFromGroup: boolean | undefined, left: boolean | undefined, conversationId: string ): JSX.Element | null { if (showUpdateGroupName(Boolean(isAdmin), Boolean(isKickedFromGroup), Boolean(left))) { return ( { await showUpdateGroupNameByConvoId(conversationId); }} > {window.i18n('editGroup')} ); } return null; } export function getRemoveModeratorsMenuItem( isAdmin: boolean | undefined, isPublic: boolean | undefined, isKickedFromGroup: boolean | undefined, conversationId: string ): JSX.Element | null { if (showRemoveModerators(Boolean(isAdmin), Boolean(isPublic), Boolean(isKickedFromGroup))) { return ( { showRemoveModeratorsByConvoId(conversationId); }} > {window.i18n('removeModerators')} ); } return null; } export function getAddModeratorsMenuItem( isAdmin: boolean | undefined, isPublic: boolean | undefined, isKickedFromGroup: boolean | undefined, conversationId: string ): JSX.Element | null { if (showAddModerators(Boolean(isAdmin), Boolean(isPublic), Boolean(isKickedFromGroup))) { return ( { showAddModeratorsByConvoId(conversationId); }} > {window.i18n('addModerators')} ); } return null; } export function getCopyMenuItem( isPublic: boolean | undefined, isGroup: boolean | undefined, conversationId: string ): JSX.Element | null { if (showCopyId(Boolean(isPublic), Boolean(isGroup))) { const copyIdLabel = isPublic ? window.i18n('copyOpenGroupURL') : window.i18n('copySessionID'); return copyPublicKeyByConvoId(conversationId)}>{copyIdLabel}; } return null; } export function getMarkAllReadMenuItem(conversationId: string): JSX.Element | null { return ( markAllReadByConvoId(conversationId)}>{window.i18n('markAllAsRead')} ); } export function getStartCallMenuItem(conversationId: string): JSX.Element | null { if (window?.lokiFeatureFlags.useCallMessage) { const convoOut = getConversationController().get(conversationId); // we don't support calling groups const hasIncomingCall = useSelector(getHasIncomingCall); const hasOngoingCall = useSelector(getHasOngoingCall); const canCall = !(hasIncomingCall || hasOngoingCall); if (!convoOut?.isPrivate() || convoOut.isMe()) { return null; } return ( { void callRecipient(conversationId, canCall); }} > {window.i18n('menuCall')} ); } return null; } export function getDisappearingMenuItem( isPublic: boolean | undefined, isKickedFromGroup: boolean | undefined, left: boolean | undefined, isBlocked: boolean | undefined, conversationId: string ): JSX.Element | null { const timerOptions = useSelector(getTimerOptions).timerOptions; if ( showTimerOptions( Boolean(isPublic), Boolean(isKickedFromGroup), Boolean(left), Boolean(isBlocked) ) ) { // const isRtlMode = isRtlBody(); return ( // Remove the && false to make context menu work with RTL support {timerOptions.map(item => ( { await setDisappearingMessagesByConvoId(conversationId, item.value); }} > {item.name} ))} ); } return null; } export function getNotificationForConvoMenuItem({ conversationId, currentNotificationSetting, isBlocked, isKickedFromGroup, left, isPrivate, }: { isKickedFromGroup: boolean | undefined; left: boolean | undefined; isBlocked: boolean | undefined; isPrivate: boolean | undefined; currentNotificationSetting: ConversationNotificationSettingType; conversationId: string; }): JSX.Element | null { if (showNotificationConvo(Boolean(isKickedFromGroup), Boolean(left), Boolean(isBlocked))) { // 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' ? '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 {(notificationForConvoOptions || []).map(item => { const disabled = item.value === currentNotificationSetting; return ( { await setNotificationForConvoId(conversationId, item.value); }} disabled={disabled} > {item.name} ); })} ); } return null; } export function isRtlBody(): boolean { return ($('body') as any).hasClass('rtl'); } export function getBlockMenuItem( isMe: boolean | undefined, isPrivate: boolean | undefined, isBlocked: boolean | undefined, conversationId: string ): JSX.Element | null { if (showBlock(Boolean(isMe), Boolean(isPrivate))) { const blockTitle = isBlocked ? window.i18n('unblockUser') : window.i18n('blockUser'); const blockHandler = isBlocked ? () => unblockConvoById(conversationId) : () => blockConvoById(conversationId); return {blockTitle}; } return null; } export function getClearNicknameMenuItem( isMe: boolean | undefined, hasNickname: boolean | undefined, isGroup: boolean | undefined, conversationId: string ): JSX.Element | null { if (showClearNickname(Boolean(isMe), Boolean(hasNickname), Boolean(isGroup))) { return ( clearNickNameByConvoId(conversationId)}> {window.i18n('clearNickname')} ); } return null; } export function getChangeNicknameMenuItem( isMe: boolean | undefined, isGroup: boolean | undefined, conversationId: string ): JSX.Element | null { const dispatch = useDispatch(); if (showChangeNickname(Boolean(isMe), Boolean(isGroup))) { return ( { dispatch(changeNickNameModal({ conversationId })); }} > {window.i18n('changeNickname')} ); } return null; } export function getDeleteMessagesMenuItem(conversationId: string): JSX.Element | null { return ( { deleteAllMessagesByConvoIdWithConfirmation(conversationId); }} > {window.i18n('deleteMessages')} ); return null; }