From fe3cfb9e82013af6a2f5b6dd928c6a5572fd1a88 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 22 Oct 2020 16:34:41 +1100 Subject: [PATCH] move all menu to react-contexify --- stylesheets/_conversation.scss | 7 - stylesheets/_modules.scss | 78 -------- stylesheets/_rtl.scss | 2 +- stylesheets/_session.scss | 49 ++--- stylesheets/_session_conversation.scss | 5 - stylesheets/manifest.scss | 1 + ts/components/ConversationListItem.tsx | 114 ++---------- .../conversation/ConversationHeader.tsx | 172 ++---------------- ts/components/conversation/Message.tsx | 83 +++------ ts/components/session/ActionsPanel.tsx | 1 + ts/components/session/Flex.tsx | 2 +- ts/components/session/SessionSearchInput.tsx | 34 ++-- .../session/icon/SessionIconButton.tsx | 3 +- .../session/menu/ConversationHeaderMenu.tsx | 158 ++++++++++++++++ .../menu/ConversationListItemContextMenu.tsx | 112 ++++++++++++ .../session/menu}/Menu.tsx | 46 ++--- ts/session/utils/index.ts | 2 +- 17 files changed, 392 insertions(+), 477 deletions(-) create mode 100644 ts/components/session/menu/ConversationHeaderMenu.tsx create mode 100644 ts/components/session/menu/ConversationListItemContextMenu.tsx rename ts/{session/utils => components/session/menu}/Menu.tsx (84%) diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index 587912005..1f1e7cc4e 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -41,13 +41,6 @@ opacity: 1; } -.session-message { - .react-contextmenu-wrapper { - display: inline-flex; - width: 100%; - } -} - .public-chat-message-wrapper { padding-inline-start: 10px; padding-inline-end: 10px; diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 2357e99e6..314366259 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -2618,84 +2618,6 @@ outline: none; } -// Third-party module: react-contextmenu - -.react-contextmenu { - background-color: $color-light-02; - border-radius: 4px; - min-width: 160px; - padding: 0px; - padding-top: 8px; - padding-bottom: 8px; - border: 1px solid $color-dark-05; - opacity: 0; - user-select: none; - transition: opacity $session-transition-duration; -} - -.react-contextmenu--visible { - z-index: 1000; - opacity: 1; -} - -.react-contextmenu-item { - color: $color-gray-90; - - cursor: pointer; - font-size: 13px; - line-height: 18px; - white-space: nowrap; - - padding-inline-start: 16px; - padding-top: 3px; - padding-bottom: 2px; - padding-inline-end: 16px; -} - -.react-contextmenu-item--checked:before { - content: '✓'; - display: inline-block; - position: absolute; - right: 7px; - color: $color-gray-90; -} - -.react-contextmenu-item.react-contextmenu-submenu { - padding: 0; -} - -.react-contextmenu-item.react-contextmenu-submenu > .react-contextmenu-item { - padding-right: 36px; - // We will probably need to make this padding-inline-end once the whole app is working on rtl -} - -.react-contextmenu-item.react-contextmenu-submenu - > .react-contextmenu-item:after { - content: '\25B6'; - display: inline-block; - position: absolute; - right: 7px; - color: $color-gray-90; -} - -.react-contextmenu-item.react-contextmenu-item--active, -.react-contextmenu-item.react-contextmenu-item--selected { - color: $color-white; - background-color: $color-light-35; -} - -.react-contextmenu-item.react-contextmenu-item--active.react-contextmenu-item--checked:before, -.react-contextmenu-item.react-contextmenu-item--selected.react-contextmenu-item--checked:before { - color: $color-white; -} - -.react-contextmenu-item.react-contextmenu-submenu - > .react-contextmenu-item.react-contextmenu-item--active:after, -.react-contextmenu-item.react-contextmenu-submenu - > .react-contextmenu-item.react-contextmenu-item--selected:after { - color: $color-white; -} - // All media query widths have 300px added to account for the left pane // And have been tweaked for smoother transitions diff --git a/stylesheets/_rtl.scss b/stylesheets/_rtl.scss index 9b7cf7187..63b400bc6 100644 --- a/stylesheets/_rtl.scss +++ b/stylesheets/_rtl.scss @@ -6,7 +6,7 @@ body.rtl { .group-settings-item, .contact-selection-list, .group-member-list__selection, - .react-contextmenu-item, + .react-contexify__item, .session-settings-list { direction: rtl; } diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index 5cc36cdb9..e1e7f040c 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -753,41 +753,28 @@ label { } } -.react-contextmenu { - padding: 0px; - margin: 0px; +.react-contexify { + z-index: 3; + min-width: 200px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3), 0 0 0 1px #eee; - border: none !important; - border-radius: 0px; -} - -.react-contextmenu-item { - display: flex; - align-items: center; - transition: $session-transition-duration; - - height: 25px; - padding: $session-margin-md $session-margin-sm; - - @include themify($themes) { - background: themed('contextMenuBackground'); - color: themed('textColor'); - } - font-family: $session-font-accent; - font-size: $session-font-sm; - line-height: $session-icon-size-sm; - font-weight: 700; - - &--active, - &--selected { + .react-contexify__item:not(.react-contexify__item--disabled):hover + > .react-contexify__item__content { @include themify($themes) { - background: themed('clickableHovered'); + background: themed('accent'); + color: themed('textColorOpposite'); } } - &:active, - &:visited, - &:focus { - outline: none; + .react-contexify__item__content { + transition: $session-transition-duration; + } + + &.react-contexify__submenu { + top: -28px !important; // height of an item element + } + + .react-contexify__submenu-arrow { + line-height: 16px; // center the arrow for submenu } } diff --git a/stylesheets/_session_conversation.scss b/stylesheets/_session_conversation.scss index 3c1cd7dc2..73b2fc681 100644 --- a/stylesheets/_session_conversation.scss +++ b/stylesheets/_session_conversation.scss @@ -197,11 +197,6 @@ letter-spacing: 0.03em; margin-top: 3px; margin-bottom: 3px; - - .react-contextmenu-wrapper { - display: flex; - align-items: start; - } } .composition-container { diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 8c3179dbb..8faf2ace5 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -1,6 +1,7 @@ // Modules @import 'node_modules/emoji-mart/css/emoji-mart.css'; @import 'node_modules/react-h5-audio-player/lib/styles.css'; +@import 'node_modules/react-contexify/dist/ReactContexify.min.css'; // Global Settings, Variables, and Mixins @import 'themes.scss'; diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 48aa0471d..2c2197440 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -1,8 +1,7 @@ import React from 'react'; import classNames from 'classnames'; import { isEmpty } from 'lodash'; -import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu'; -import { Portal } from 'react-portal'; +import { MenuProvider } from 'react-contexify'; import { Avatar } from './Avatar'; import { MessageBody } from './conversation/MessageBody'; @@ -11,20 +10,15 @@ import { ContactName } from './conversation/ContactName'; import { TypingAnimation } from './conversation/TypingAnimation'; import { LocalizerType } from '../types/Util'; -import { - getBlockMenuItem, - getClearNicknameMenuItem, - getCopyMenuItem, - getDeleteContactMenuItem, - getDeleteMessagesMenuItem, - getInviteContactMenuItem, - getLeaveGroupMenuItem, -} from '../session/utils/Menu'; import { ConversationAvatar, usingClosedConversationDetails, } from './session/usingClosedConversationDetails'; +import { + ConversationListItemContextMenu, + PropsContextConversationItem, +} from './session/menu/ConversationListItemContextMenu'; export type PropsData = { id: string; @@ -159,86 +153,6 @@ class ConversationListItem extends React.PureComponent { ); } - public renderContextMenu(triggerId: string) { - const { - i18n, - isBlocked, - isMe, - isClosable, - isRss, - isPublic, - hasNickname, - type, - isKickedFromGroup, - onDeleteContact, - onDeleteMessages, - onBlockContact, - onClearNickname, - onCopyPublicKey, - onUnblockContact, - onInviteContacts, - } = this.props; - - const isPrivate = type === 'direct'; - - return ( - - {getBlockMenuItem( - isMe, - isPrivate, - isBlocked, - onBlockContact, - onUnblockContact, - i18n - )} - {/* {!isPublic && !isRss && !isMe ? ( - - {i18n('changeNickname')} - - ) : null} */} - {getClearNicknameMenuItem( - isPublic, - isRss, - isMe, - hasNickname, - onClearNickname, - i18n - )} - {getCopyMenuItem( - isPublic, - isRss, - type === 'group', - onCopyPublicKey, - i18n - )} - {getDeleteMessagesMenuItem(isPublic, onDeleteMessages, i18n)} - {getInviteContactMenuItem( - type === 'group', - isPublic, - onInviteContacts, - i18n - )} - {getDeleteContactMenuItem( - isMe, - isClosable, - type === 'group', - isPublic, - isRss, - onDeleteContact, - i18n - )} - {getLeaveGroupMenuItem( - isKickedFromGroup, - type === 'group', - isPublic, - isRss, - onDeleteContact, - i18n - )} - - ); - } - public renderMessage() { const { lastMessage, isTyping, unreadCount, i18n } = this.props; @@ -302,11 +216,12 @@ class ConversationListItem extends React.PureComponent { style, mentionedUs, } = this.props; - const triggerId = `conversation-item-${phoneNumber}-ctxmenu-${Date.now()}`; + const triggerId = `conversation-item-${phoneNumber}-ctxmenu`; + const key = `conversation-item-${phoneNumber}`; return ( -
- +
+
{ @@ -333,12 +248,19 @@ class ConversationListItem extends React.PureComponent { {this.renderMessage()}
-
- {this.renderContextMenu(triggerId)} + +
); } + private getMenuProps(triggerId: string): PropsContextConversationItem { + return { + triggerId, + ...this.props, + }; + } + private renderUser() { const { name, phoneNumber, profileName, isMe, i18n } = this.props; diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 13b80068b..ca48f26c6 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { Avatar } from '../Avatar'; -import { Colors, LocalizerType } from '../../types/Util'; -import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu'; +import { LocalizerType } from '../../types/Util'; import { + SessionIcon, SessionIconButton, SessionIconSize, SessionIconType, @@ -15,11 +15,15 @@ import { SessionButtonColor, SessionButtonType, } from '../session/SessionButton'; -import * as Menu from '../../session/utils/Menu'; import { ConversationAvatar, usingClosedConversationDetails, } from '../session/usingClosedConversationDetails'; +import { MenuProvider } from 'react-contexify'; +import { + ConversationHeaderMenu, + PropsConversationHeaderMenu, +} from '../session/menu/ConversationHeaderMenu'; export interface TimerOption { name: string; @@ -93,24 +97,14 @@ interface Props { } class ConversationHeader extends React.Component { - public showMenuBound: (event: React.MouseEvent) => void; public onAvatarClickBound: (userPubKey: string) => void; - public menuTriggerRef: React.RefObject; public constructor(props: Props) { super(props); - this.menuTriggerRef = React.createRef(); - this.showMenuBound = this.showMenu.bind(this); this.onAvatarClickBound = this.onAvatarClick.bind(this); } - public showMenu(event: React.MouseEvent) { - if (this.menuTriggerRef.current) { - this.menuTriggerRef.current.handleContextClick(event); - } - } - public renderBackButton() { const { onGoBack, showBackButton } = this.props; @@ -246,90 +240,13 @@ class ConversationHeader extends React.Component { if (showBackButton) { return null; } - return ( - + - - ); - } - - public renderMenu(triggerId: string) { - const { - i18n, - isMe, - isClosable, - isPublic, - isRss, - isGroup, - isKickedFromGroup, - amMod, - onDeleteMessages, - onDeleteContact, - onCopyPublicKey, - onLeaveGroup, - onAddModerators, - onRemoveModerators, - onInviteContacts, - onUpdateGroupName, - } = this.props; - - return ( - - {this.renderPublicMenuItems()} - {Menu.getCopyMenuItem(isPublic, isRss, isGroup, onCopyPublicKey, i18n)} - {Menu.getDeleteMessagesMenuItem(isPublic, onDeleteMessages, i18n)} - {Menu.getAddModeratorsMenuItem( - amMod, - isKickedFromGroup, - onAddModerators, - i18n - )} - {Menu.getRemoveModeratorsMenuItem( - amMod, - isKickedFromGroup, - onRemoveModerators, - i18n - )} - {Menu.getUpdateGroupNameMenuItem( - amMod, - isKickedFromGroup, - onUpdateGroupName, - i18n - )} - {Menu.getLeaveGroupMenuItem( - isKickedFromGroup, - isGroup, - isPublic, - isRss, - onLeaveGroup, - i18n - )} - {/* TODO: add delete group */} - {Menu.getInviteContactMenuItem( - isGroup, - isPublic, - onInviteContacts, - i18n - )} - {Menu.getDeleteContactMenuItem( - isMe, - isClosable, - isGroup, - isPublic, - isRss, - onDeleteContact, - i18n - )} - + ); } @@ -369,7 +286,7 @@ class ConversationHeader extends React.Component { public render() { const { id, isKickedFromGroup } = this.props; - const triggerId = `conversation-header-${id}-${Date.now()}`; + const triggerId = `conversation-header-${id}`; const selectionMode = !!this.props.selectedMessages.length; return ( @@ -386,7 +303,7 @@ class ConversationHeader extends React.Component { {!this.props.isRss && !selectionMode && this.renderAvatar()} - {!selectionMode && this.renderMenu(triggerId)} + {selectionMode && this.renderSelectionOverlay()} @@ -406,68 +323,11 @@ class ConversationHeader extends React.Component { ($('.session-search-input input') as any).focus(); } - // tslint:disable-next-line: cyclomatic-complexity - private renderPublicMenuItems() { - const { - i18n, - isBlocked, - isMe, - isGroup, - isPrivate, - isKickedFromGroup, - isPublic, - isRss, - onResetSession, - onSetDisappearingMessages, - onShowSafetyNumber, - timerOptions, - onBlockUser, - onUnblockUser, - } = this.props; - - const disappearingMessagesMenuItem = Menu.getDisappearingMenuItem( - isPublic, - isRss, - isKickedFromGroup, - isBlocked, - timerOptions, - onSetDisappearingMessages, - i18n - ); - - const showSafetyNumberMenuItem = Menu.getShowSafetyNumberMenuItem( - isPublic, - isRss, - isGroup, - isMe, - onShowSafetyNumber, - i18n - ); - const resetSessionMenuItem = Menu.getResetSessionMenuItem( - isPublic, - isRss, - isGroup, - isBlocked, - onResetSession, - i18n - ); - const blockHandlerMenuItem = Menu.getBlockMenuItem( - isMe, - isPrivate, - isBlocked, - onBlockUser, - onUnblockUser, - i18n - ); - - return ( - - {disappearingMessagesMenuItem} - {showSafetyNumberMenuItem} - {resetSessionMenuItem} - {blockHandlerMenuItem} - - ); + private getHeaderMenuProps(triggerId: string): PropsConversationHeaderMenu { + return { + triggerId, + ...this.props, + }; } } diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 6101ba6cb..e8d76f32e 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -33,11 +33,9 @@ import { Contact } from '../../types/Contact'; import { getIncrement } from '../../util/timer'; import { isFileDangerous } from '../../util/isFileDangerous'; -import { ColorType, LocalizerType } from '../../types/Util'; -import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu'; import { SessionIcon, SessionIconSize, SessionIconType } from '../session/icon'; import _ from 'lodash'; -import { MessageModel } from '../../../js/models/messages'; +import { animation, Item, Menu, MenuProvider, theme } from 'react-contexify'; declare global { interface Window { @@ -133,7 +131,6 @@ const EXPIRED_DELAY = 600; export class Message extends React.PureComponent { public captureMenuTriggerBound: (trigger: any) => void; - public showMenuBound: (event: React.MouseEvent) => void; public handleImageErrorBound: () => void; public menuTriggerRef: Trigger | undefined; @@ -144,7 +141,6 @@ export class Message extends React.PureComponent { super(props); this.captureMenuTriggerBound = this.captureMenuTrigger.bind(this); - this.showMenuBound = this.showMenu.bind(this); this.handleImageErrorBound = this.handleImageError.bind(this); this.onReplyPrivate = this.onReplyPrivate.bind(this); @@ -805,7 +801,6 @@ export class Message extends React.PureComponent { isDeletable, onDelete, onDownload, - onReply, onRetrySend, onShowDetail, isPublic, @@ -821,8 +816,8 @@ export class Message extends React.PureComponent { // Wraps a function to prevent event propagation, thus preventing // message selection whenever any of the menu buttons are pressed. - const wrap = (f: any, ...args: Array) => (event: Event) => { - event.stopPropagation(); + const wrap = (f: any, ...args: Array) => (e: any) => { + e.event.stopPropagation(); if (f) { f(...args); } @@ -847,72 +842,42 @@ export class Message extends React.PureComponent { ); return ( - {!multipleAttachments && attachments && attachments[0] ? ( - { - e.stopPropagation(); + { + e.event.stopPropagation(); if (onDownload) { onDownload(isDangerous); } }} > {window.i18n('downloadAttachment')} - + ) : null} - - {window.i18n('copyMessage')} - - + {window.i18n('copyMessage')} + {window.i18n('replyToMessage')} - - + + {window.i18n('moreInformation')} - + {showRetry ? ( - - {window.i18n('resend')} - + {window.i18n('resend')} ) : null} {isDeletable ? ( - - {deleteMessageCtxText} - + {deleteMessageCtxText} ) : null} {isModerator && isPublic ? ( - - {window.i18n('banUser')} - + {window.i18n('banUser')} ) : null} - + ); } @@ -1036,7 +1001,7 @@ export class Message extends React.PureComponent { return (
- + {this.renderAvatar()}
{ ? this.renderContextMenu(rightClickTriggerId) : null}
-
+
); } @@ -1147,8 +1112,8 @@ export class Message extends React.PureComponent { ); } - private onReplyPrivate(e: Event) { - e.stopPropagation(); + private onReplyPrivate(e: any) { + e.event.stopPropagation(); if (this.props && this.props.onReply) { this.props.onReply(this.props.timestamp); } diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 8852e1a8d..04e9cd35f 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -6,6 +6,7 @@ import { PropsData as ConversationListItemPropsType } from '../ConversationListI import { createOrUpdateItem, getItemById } from '../../../js/modules/data'; import { APPLY_THEME } from '../../state/ducks/theme'; import { darkTheme, lightTheme } from '../../state/ducks/SessionTheme'; +// tslint:disable-next-line: no-import-side-effect no-submodule-imports export enum SectionType { Profile, diff --git a/ts/components/session/Flex.tsx b/ts/components/session/Flex.tsx index 699e85f9d..eba869840 100644 --- a/ts/components/session/Flex.tsx +++ b/ts/components/session/Flex.tsx @@ -1,4 +1,4 @@ -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; export interface FlexProps { children?: any; diff --git a/ts/components/session/SessionSearchInput.tsx b/ts/components/session/SessionSearchInput.tsx index ddbc71a53..1b084c706 100644 --- a/ts/components/session/SessionSearchInput.tsx +++ b/ts/components/session/SessionSearchInput.tsx @@ -1,6 +1,6 @@ import React from 'react'; +import { animation, Item, Menu, MenuProvider } from 'react-contexify'; import { SessionIconButton, SessionIconSize, SessionIconType } from './icon'; -import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu'; interface Props { searchString: string; @@ -21,7 +21,7 @@ export class SessionSearchInput extends React.Component { return ( <> - +
{ placeholder={this.props.placeholder} />
-
- - document.execCommand('undo')}> + + + document.execCommand('undo')}> {window.i18n('editMenuUndo')} - - document.execCommand('redo')}> + + document.execCommand('redo')}> {window.i18n('editMenuRedo')} - +
- document.execCommand('cut')}> + document.execCommand('cut')}> {window.i18n('editMenuCut')} - - document.execCommand('copy')}> + + document.execCommand('copy')}> {window.i18n('editMenuCopy')} - - document.execCommand('paste')}> + + document.execCommand('paste')}> {window.i18n('editMenuPaste')} - - document.execCommand('selectAll')}> + + document.execCommand('selectAll')}> {window.i18n('editMenuSelectAll')} - - + +
); } diff --git a/ts/components/session/icon/SessionIconButton.tsx b/ts/components/session/icon/SessionIconButton.tsx index 60dfb083d..2a6128972 100644 --- a/ts/components/session/icon/SessionIconButton.tsx +++ b/ts/components/session/icon/SessionIconButton.tsx @@ -5,14 +5,13 @@ import { Props, SessionIcon } from '../icon'; import { SessionNotificationCount } from '../SessionNotificationCount'; interface SProps extends Props { - onClick: any; + onClick?: any; notificationCount?: number; isSelected: boolean; } export class SessionIconButton extends React.PureComponent { public static readonly extendedDefaults = { - onClick: () => null, notificationCount: undefined, isSelected: false, }; diff --git a/ts/components/session/menu/ConversationHeaderMenu.tsx b/ts/components/session/menu/ConversationHeaderMenu.tsx new file mode 100644 index 000000000..2d776e219 --- /dev/null +++ b/ts/components/session/menu/ConversationHeaderMenu.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import { animation, Menu } from 'react-contexify'; +import { + getAddModeratorsMenuItem, + getBlockMenuItem, + getCopyMenuItem, + getDeleteContactMenuItem, + getDeleteMessagesMenuItem, + getDisappearingMenuItem, + getInviteContactMenuItem, + getLeaveGroupMenuItem, + getRemoveModeratorsMenuItem, + getResetSessionMenuItem, + getShowSafetyNumberMenuItem, + getUpdateGroupNameMenuItem, +} from './Menu'; +import { TimerOption } from '../../conversation/ConversationHeader'; + +export type PropsConversationHeaderMenu = { + triggerId: string; + isMe: boolean; + isPublic?: boolean; + isRss?: boolean; + isClosable?: boolean; + isKickedFromGroup?: boolean; + isGroup: boolean; + amMod: boolean; + timerOptions: Array; + isPrivate: boolean; + isBlocked: boolean; + onDeleteMessages?: () => void; + onDeleteContact?: () => void; + onCopyPublicKey?: () => void; + onInviteContacts?: () => void; + + onLeaveGroup: () => void; + onAddModerators: () => void; + onRemoveModerators: () => void; + onUpdateGroupName: () => void; + onBlockUser: () => void; + onUnblockUser: () => void; + onShowSafetyNumber: () => void; + onSetDisappearingMessages: (seconds: number) => void; + onResetSession: () => void; +}; + +export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { + const { + triggerId, + isMe, + isClosable, + isPublic, + isRss, + isGroup, + isKickedFromGroup, + amMod, + timerOptions, + isBlocked, + isPrivate, + + onDeleteMessages, + onDeleteContact, + onCopyPublicKey, + onLeaveGroup, + onAddModerators, + onRemoveModerators, + onInviteContacts, + onUpdateGroupName, + onBlockUser, + onUnblockUser, + onShowSafetyNumber, + onResetSession, + onSetDisappearingMessages, + } = props; + + return ( + + {getDisappearingMenuItem( + isPublic, + isRss, + isKickedFromGroup, + isBlocked, + timerOptions, + onSetDisappearingMessages, + window.i18n + )} + {getShowSafetyNumberMenuItem( + isPublic, + isRss, + isGroup, + isMe, + onShowSafetyNumber, + window.i18n + )} + {getResetSessionMenuItem( + isPublic, + isRss, + isGroup, + isBlocked, + onResetSession, + window.i18n + )} + {getBlockMenuItem( + isMe, + isPrivate, + isBlocked, + onBlockUser, + onUnblockUser, + window.i18n + )} + + {getCopyMenuItem(isPublic, isRss, isGroup, onCopyPublicKey, window.i18n)} + {getDeleteMessagesMenuItem(isPublic, onDeleteMessages, window.i18n)} + {getAddModeratorsMenuItem( + amMod, + isKickedFromGroup, + onAddModerators, + window.i18n + )} + {getRemoveModeratorsMenuItem( + amMod, + isKickedFromGroup, + onRemoveModerators, + window.i18n + )} + {getUpdateGroupNameMenuItem( + amMod, + isKickedFromGroup, + onUpdateGroupName, + window.i18n + )} + {getLeaveGroupMenuItem( + isKickedFromGroup, + isGroup, + isPublic, + isRss, + onLeaveGroup, + window.i18n + )} + {/* TODO: add delete group */} + {getInviteContactMenuItem( + isGroup, + isPublic, + onInviteContacts, + window.i18n + )} + {getDeleteContactMenuItem( + isMe, + isClosable, + isGroup, + isPublic, + isRss, + onDeleteContact, + window.i18n + )} + + ); +}; diff --git a/ts/components/session/menu/ConversationListItemContextMenu.tsx b/ts/components/session/menu/ConversationListItemContextMenu.tsx new file mode 100644 index 000000000..87bbca8d8 --- /dev/null +++ b/ts/components/session/menu/ConversationListItemContextMenu.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { animation, Menu } from 'react-contexify'; + +import { + getBlockMenuItem, + getClearNicknameMenuItem, + getCopyMenuItem, + getDeleteContactMenuItem, + getDeleteMessagesMenuItem, + getInviteContactMenuItem, + getLeaveGroupMenuItem, +} from './Menu'; + +export type PropsContextConversationItem = { + triggerId: string; + type: 'group' | 'direct'; + isMe: boolean; + isPublic?: boolean; + isRss?: boolean; + isClosable?: boolean; + isBlocked?: boolean; + hasNickname?: boolean; + isKickedFromGroup?: boolean; + + onDeleteMessages?: () => void; + onDeleteContact?: () => void; + onBlockContact?: () => void; + onCopyPublicKey?: () => void; + onUnblockContact?: () => void; + onInviteContacts?: () => void; + onClearNickname?: () => void; +}; + +export const ConversationListItemContextMenu = ( + props: PropsContextConversationItem +) => { + const { + triggerId, + isBlocked, + isMe, + isClosable, + isRss, + isPublic, + hasNickname, + type, + isKickedFromGroup, + onDeleteContact, + onDeleteMessages, + onBlockContact, + onClearNickname, + onCopyPublicKey, + onUnblockContact, + onInviteContacts, + } = props; + + return ( + + {getBlockMenuItem( + isMe, + type === 'direct', + isBlocked, + onBlockContact, + onUnblockContact, + window.i18n + )} + {/* {!isPublic && !isRss && !isMe ? ( + + {i18n('changeNickname')} + + ) : null} */} + {getClearNicknameMenuItem( + isPublic, + isRss, + isMe, + hasNickname, + onClearNickname, + window.i18n + )} + {getCopyMenuItem( + isPublic, + isRss, + type === 'group', + onCopyPublicKey, + window.i18n + )} + {getDeleteMessagesMenuItem(isPublic, onDeleteMessages, window.i18n)} + {getInviteContactMenuItem( + type === 'group', + isPublic, + onInviteContacts, + window.i18n + )} + {getDeleteContactMenuItem( + isMe, + isClosable, + type === 'group', + isPublic, + isRss, + onDeleteContact, + window.i18n + )} + {getLeaveGroupMenuItem( + isKickedFromGroup, + type === 'group', + isPublic, + isRss, + onDeleteContact, + window.i18n + )} + + ); +}; diff --git a/ts/session/utils/Menu.tsx b/ts/components/session/menu/Menu.tsx similarity index 84% rename from ts/session/utils/Menu.tsx rename to ts/components/session/menu/Menu.tsx index 812cca30c..e830bc7f5 100644 --- a/ts/session/utils/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { MenuItem, SubMenu } from 'react-contextmenu'; -import { LocalizerType } from '../../types/Util'; -import { TimerOption } from '../../components/conversation/ConversationHeader'; +import { LocalizerType } from '../../../types/Util'; +import { TimerOption } from '../../conversation/ConversationHeader'; +import { Item, Submenu } from 'react-contexify'; function showTimerOptions( isPublic: boolean, @@ -116,7 +116,7 @@ export function getInviteContactMenuItem( i18n: LocalizerType ): JSX.Element | null { if (showInviteContact(Boolean(isGroup), Boolean(isPublic))) { - return {i18n('inviteContacts')}; + return {i18n('inviteContacts')}; } return null; } @@ -140,9 +140,9 @@ export function getDeleteContactMenuItem( ) ) { if (isPublic) { - return {i18n('leaveGroup')}; + return {i18n('leaveGroup')}; } - return {i18n('delete')}; + return {i18n('delete')}; } return null; } @@ -163,7 +163,7 @@ export function getLeaveGroupMenuItem( Boolean(isRss) ) ) { - return {i18n('leaveGroup')}; + return {i18n('leaveGroup')}; } return null; } @@ -175,7 +175,7 @@ export function getUpdateGroupNameMenuItem( i18n: LocalizerType ): JSX.Element | null { if (showUpdateGroupName(Boolean(amMod), Boolean(isKickedFromGroup))) { - return {i18n('editGroup')}; + return {i18n('editGroup')}; } return null; } @@ -187,7 +187,7 @@ export function getRemoveModeratorsMenuItem( i18n: LocalizerType ): JSX.Element | null { if (showRemoveModerators(Boolean(amMod), Boolean(isKickedFromGroup))) { - return {i18n('removeModerators')}; + return {i18n('removeModerators')}; } return null; } @@ -199,7 +199,7 @@ export function getAddModeratorsMenuItem( i18n: LocalizerType ): JSX.Element | null { if (showAddModerators(Boolean(amMod), Boolean(isKickedFromGroup))) { - return {i18n('addModerators')}; + return {i18n('addModerators')}; } return null; } @@ -213,7 +213,7 @@ export function getCopyMenuItem( ): JSX.Element | null { if (showCopyId(Boolean(isPublic), Boolean(isRss), Boolean(isGroup))) { const copyIdLabel = i18n('copySessionID'); - return {copyIdLabel}; + return {copyIdLabel}; } return null; } @@ -238,21 +238,21 @@ export function getDisappearingMenuItem( const isRtlMode = isRtlBody(); return ( // Remove the && false to make context menu work with RTL support - {(timerOptions || []).map(item => ( - { action(item.value); }} > {item.name} - + ))} - + ); } return null; @@ -270,7 +270,7 @@ export function getShowMemberMenuItem( i18n: LocalizerType ): JSX.Element | null { if (showMemberMenu(Boolean(isPublic), Boolean(isRss), Boolean(isGroup))) { - return {i18n('groupMembers')}; + return {i18n('groupMembers')}; } return null; } @@ -291,7 +291,7 @@ export function getShowSafetyNumberMenuItem( Boolean(isMe) ) ) { - return {i18n('showSafetyNumber')}; + return {i18n('showSafetyNumber')}; } return null; } @@ -312,7 +312,7 @@ export function getResetSessionMenuItem( Boolean(isBlocked) ) ) { - return {i18n('resetSession')}; + return {i18n('resetSession')}; } return null; } @@ -328,7 +328,7 @@ export function getBlockMenuItem( if (showBlock(Boolean(isMe), Boolean(isPrivate))) { const blockTitle = isBlocked ? i18n('unblockUser') : i18n('blockUser'); const blockHandler = isBlocked ? actionUnblock : actionBlock; - return {blockTitle}; + return {blockTitle}; } return null; } @@ -349,7 +349,7 @@ export function getClearNicknameMenuItem( Boolean(hasNickname) ) ) { - return {i18n('clearNickname')}; + return {i18n('clearNickname')}; } return null; } @@ -360,7 +360,7 @@ export function getDeleteMessagesMenuItem( i18n: LocalizerType ): JSX.Element | null { if (showDeleteMessages(Boolean(isPublic))) { - return {i18n('deleteMessages')}; + return {i18n('deleteMessages')}; } return null; } diff --git a/ts/session/utils/index.ts b/ts/session/utils/index.ts index bf9e1f969..88e76f5c6 100644 --- a/ts/session/utils/index.ts +++ b/ts/session/utils/index.ts @@ -5,7 +5,7 @@ import * as StringUtils from './String'; import * as NumberUtils from './Number'; import * as PromiseUtils from './Promise'; import * as ProtobufUtils from './Protobuf'; -import * as MenuUtils from './Menu'; +import * as MenuUtils from '../../components/session/menu/Menu'; import * as ToastUtils from './Toast'; export * from './Attachments';