move all menu to react-contexify

pull/1387/head
Audric Ackermann 5 years ago
parent 78843e81dd
commit fe3cfb9e82
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -41,13 +41,6 @@
opacity: 1; opacity: 1;
} }
.session-message {
.react-contextmenu-wrapper {
display: inline-flex;
width: 100%;
}
}
.public-chat-message-wrapper { .public-chat-message-wrapper {
padding-inline-start: 10px; padding-inline-start: 10px;
padding-inline-end: 10px; padding-inline-end: 10px;

@ -2618,84 +2618,6 @@
outline: none; 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 // All media query widths have 300px added to account for the left pane
// And have been tweaked for smoother transitions // And have been tweaked for smoother transitions

@ -6,7 +6,7 @@ body.rtl {
.group-settings-item, .group-settings-item,
.contact-selection-list, .contact-selection-list,
.group-member-list__selection, .group-member-list__selection,
.react-contextmenu-item, .react-contexify__item,
.session-settings-list { .session-settings-list {
direction: rtl; direction: rtl;
} }

@ -753,41 +753,28 @@ label {
} }
} }
.react-contextmenu { .react-contexify {
padding: 0px; z-index: 3;
margin: 0px; min-width: 200px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3), 0 0 0 1px #eee;
border: none !important; .react-contexify__item:not(.react-contexify__item--disabled):hover
border-radius: 0px; > .react-contexify__item__content {
@include themify($themes) {
background: themed('accent');
color: themed('textColorOpposite');
} }
}
.react-contextmenu-item { .react-contexify__item__content {
display: flex;
align-items: center;
transition: $session-transition-duration; 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, &.react-contexify__submenu {
&--selected { top: -28px !important; // height of an item element
@include themify($themes) {
background: themed('clickableHovered');
}
} }
&:active,
&:visited, .react-contexify__submenu-arrow {
&:focus { line-height: 16px; // center the arrow for submenu
outline: none;
} }
} }

@ -197,11 +197,6 @@
letter-spacing: 0.03em; letter-spacing: 0.03em;
margin-top: 3px; margin-top: 3px;
margin-bottom: 3px; margin-bottom: 3px;
.react-contextmenu-wrapper {
display: flex;
align-items: start;
}
} }
.composition-container { .composition-container {

@ -1,6 +1,7 @@
// Modules // Modules
@import 'node_modules/emoji-mart/css/emoji-mart.css'; @import 'node_modules/emoji-mart/css/emoji-mart.css';
@import 'node_modules/react-h5-audio-player/lib/styles.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 // Global Settings, Variables, and Mixins
@import 'themes.scss'; @import 'themes.scss';

@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu'; import { MenuProvider } from 'react-contexify';
import { Portal } from 'react-portal';
import { Avatar } from './Avatar'; import { Avatar } from './Avatar';
import { MessageBody } from './conversation/MessageBody'; import { MessageBody } from './conversation/MessageBody';
@ -11,20 +10,15 @@ import { ContactName } from './conversation/ContactName';
import { TypingAnimation } from './conversation/TypingAnimation'; import { TypingAnimation } from './conversation/TypingAnimation';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import {
getBlockMenuItem,
getClearNicknameMenuItem,
getCopyMenuItem,
getDeleteContactMenuItem,
getDeleteMessagesMenuItem,
getInviteContactMenuItem,
getLeaveGroupMenuItem,
} from '../session/utils/Menu';
import { import {
ConversationAvatar, ConversationAvatar,
usingClosedConversationDetails, usingClosedConversationDetails,
} from './session/usingClosedConversationDetails'; } from './session/usingClosedConversationDetails';
import {
ConversationListItemContextMenu,
PropsContextConversationItem,
} from './session/menu/ConversationListItemContextMenu';
export type PropsData = { export type PropsData = {
id: string; id: string;
@ -159,86 +153,6 @@ class ConversationListItem extends React.PureComponent<Props> {
); );
} }
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 (
<ContextMenu id={triggerId}>
{getBlockMenuItem(
isMe,
isPrivate,
isBlocked,
onBlockContact,
onUnblockContact,
i18n
)}
{/* {!isPublic && !isRss && !isMe ? (
<MenuItem onClick={onChangeNickname}>
{i18n('changeNickname')}
</MenuItem>
) : 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
)}
</ContextMenu>
);
}
public renderMessage() { public renderMessage() {
const { lastMessage, isTyping, unreadCount, i18n } = this.props; const { lastMessage, isTyping, unreadCount, i18n } = this.props;
@ -302,11 +216,12 @@ class ConversationListItem extends React.PureComponent<Props> {
style, style,
mentionedUs, mentionedUs,
} = this.props; } = this.props;
const triggerId = `conversation-item-${phoneNumber}-ctxmenu-${Date.now()}`; const triggerId = `conversation-item-${phoneNumber}-ctxmenu`;
const key = `conversation-item-${phoneNumber}`;
return ( return (
<div key={triggerId}> <div key={key}>
<ContextMenuTrigger id={triggerId}> <MenuProvider id={triggerId}>
<div <div
role="button" role="button"
onClick={() => { onClick={() => {
@ -333,12 +248,19 @@ class ConversationListItem extends React.PureComponent<Props> {
{this.renderMessage()} {this.renderMessage()}
</div> </div>
</div> </div>
</ContextMenuTrigger> </MenuProvider>
<Portal>{this.renderContextMenu(triggerId)}</Portal> <ConversationListItemContextMenu {...this.getMenuProps(triggerId)} />
</div> </div>
); );
} }
private getMenuProps(triggerId: string): PropsContextConversationItem {
return {
triggerId,
...this.props,
};
}
private renderUser() { private renderUser() {
const { name, phoneNumber, profileName, isMe, i18n } = this.props; const { name, phoneNumber, profileName, isMe, i18n } = this.props;

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { Avatar } from '../Avatar'; import { Avatar } from '../Avatar';
import { Colors, LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
import { import {
SessionIcon,
SessionIconButton, SessionIconButton,
SessionIconSize, SessionIconSize,
SessionIconType, SessionIconType,
@ -15,11 +15,15 @@ import {
SessionButtonColor, SessionButtonColor,
SessionButtonType, SessionButtonType,
} from '../session/SessionButton'; } from '../session/SessionButton';
import * as Menu from '../../session/utils/Menu';
import { import {
ConversationAvatar, ConversationAvatar,
usingClosedConversationDetails, usingClosedConversationDetails,
} from '../session/usingClosedConversationDetails'; } from '../session/usingClosedConversationDetails';
import { MenuProvider } from 'react-contexify';
import {
ConversationHeaderMenu,
PropsConversationHeaderMenu,
} from '../session/menu/ConversationHeaderMenu';
export interface TimerOption { export interface TimerOption {
name: string; name: string;
@ -93,24 +97,14 @@ interface Props {
} }
class ConversationHeader extends React.Component<Props> { class ConversationHeader extends React.Component<Props> {
public showMenuBound: (event: React.MouseEvent<HTMLDivElement>) => void;
public onAvatarClickBound: (userPubKey: string) => void; public onAvatarClickBound: (userPubKey: string) => void;
public menuTriggerRef: React.RefObject<any>;
public constructor(props: Props) { public constructor(props: Props) {
super(props); super(props);
this.menuTriggerRef = React.createRef();
this.showMenuBound = this.showMenu.bind(this);
this.onAvatarClickBound = this.onAvatarClick.bind(this); this.onAvatarClickBound = this.onAvatarClick.bind(this);
} }
public showMenu(event: React.MouseEvent<HTMLDivElement>) {
if (this.menuTriggerRef.current) {
this.menuTriggerRef.current.handleContextClick(event);
}
}
public renderBackButton() { public renderBackButton() {
const { onGoBack, showBackButton } = this.props; const { onGoBack, showBackButton } = this.props;
@ -246,90 +240,13 @@ class ConversationHeader extends React.Component<Props> {
if (showBackButton) { if (showBackButton) {
return null; return null;
} }
return ( return (
<ContextMenuTrigger <MenuProvider id={triggerId} event="onClick">
id={triggerId}
ref={this.menuTriggerRef}
holdToDisplay={1}
>
<SessionIconButton <SessionIconButton
iconType={SessionIconType.Ellipses} iconType={SessionIconType.Ellipses}
iconSize={SessionIconSize.Medium} iconSize={SessionIconSize.Medium}
onClick={this.showMenuBound}
/> />
</ContextMenuTrigger> </MenuProvider>
);
}
public renderMenu(triggerId: string) {
const {
i18n,
isMe,
isClosable,
isPublic,
isRss,
isGroup,
isKickedFromGroup,
amMod,
onDeleteMessages,
onDeleteContact,
onCopyPublicKey,
onLeaveGroup,
onAddModerators,
onRemoveModerators,
onInviteContacts,
onUpdateGroupName,
} = this.props;
return (
<ContextMenu id={triggerId}>
{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
)}
</ContextMenu>
); );
} }
@ -369,7 +286,7 @@ class ConversationHeader extends React.Component<Props> {
public render() { public render() {
const { id, isKickedFromGroup } = this.props; const { id, isKickedFromGroup } = this.props;
const triggerId = `conversation-header-${id}-${Date.now()}`; const triggerId = `conversation-header-${id}`;
const selectionMode = !!this.props.selectedMessages.length; const selectionMode = !!this.props.selectedMessages.length;
return ( return (
@ -386,7 +303,7 @@ class ConversationHeader extends React.Component<Props> {
{!this.props.isRss && !selectionMode && this.renderAvatar()} {!this.props.isRss && !selectionMode && this.renderAvatar()}
{!selectionMode && this.renderMenu(triggerId)} <ConversationHeaderMenu {...this.getHeaderMenuProps(triggerId)} />
</div> </div>
{selectionMode && this.renderSelectionOverlay()} {selectionMode && this.renderSelectionOverlay()}
@ -406,68 +323,11 @@ class ConversationHeader extends React.Component<Props> {
($('.session-search-input input') as any).focus(); ($('.session-search-input input') as any).focus();
} }
// tslint:disable-next-line: cyclomatic-complexity private getHeaderMenuProps(triggerId: string): PropsConversationHeaderMenu {
private renderPublicMenuItems() { return {
const { triggerId,
i18n, ...this.props,
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 (
<React.Fragment>
{disappearingMessagesMenuItem}
{showSafetyNumberMenuItem}
{resetSessionMenuItem}
{blockHandlerMenuItem}
</React.Fragment>
);
} }
} }

@ -33,11 +33,9 @@ import { Contact } from '../../types/Contact';
import { getIncrement } from '../../util/timer'; import { getIncrement } from '../../util/timer';
import { isFileDangerous } from '../../util/isFileDangerous'; 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 { SessionIcon, SessionIconSize, SessionIconType } from '../session/icon';
import _ from 'lodash'; import _ from 'lodash';
import { MessageModel } from '../../../js/models/messages'; import { animation, Item, Menu, MenuProvider, theme } from 'react-contexify';
declare global { declare global {
interface Window { interface Window {
@ -133,7 +131,6 @@ const EXPIRED_DELAY = 600;
export class Message extends React.PureComponent<Props, State> { export class Message extends React.PureComponent<Props, State> {
public captureMenuTriggerBound: (trigger: any) => void; public captureMenuTriggerBound: (trigger: any) => void;
public showMenuBound: (event: React.MouseEvent<HTMLDivElement>) => void;
public handleImageErrorBound: () => void; public handleImageErrorBound: () => void;
public menuTriggerRef: Trigger | undefined; public menuTriggerRef: Trigger | undefined;
@ -144,7 +141,6 @@ export class Message extends React.PureComponent<Props, State> {
super(props); super(props);
this.captureMenuTriggerBound = this.captureMenuTrigger.bind(this); this.captureMenuTriggerBound = this.captureMenuTrigger.bind(this);
this.showMenuBound = this.showMenu.bind(this);
this.handleImageErrorBound = this.handleImageError.bind(this); this.handleImageErrorBound = this.handleImageError.bind(this);
this.onReplyPrivate = this.onReplyPrivate.bind(this); this.onReplyPrivate = this.onReplyPrivate.bind(this);
@ -805,7 +801,6 @@ export class Message extends React.PureComponent<Props, State> {
isDeletable, isDeletable,
onDelete, onDelete,
onDownload, onDownload,
onReply,
onRetrySend, onRetrySend,
onShowDetail, onShowDetail,
isPublic, isPublic,
@ -821,8 +816,8 @@ export class Message extends React.PureComponent<Props, State> {
// Wraps a function to prevent event propagation, thus preventing // Wraps a function to prevent event propagation, thus preventing
// message selection whenever any of the menu buttons are pressed. // message selection whenever any of the menu buttons are pressed.
const wrap = (f: any, ...args: Array<any>) => (event: Event) => { const wrap = (f: any, ...args: Array<any>) => (e: any) => {
event.stopPropagation(); e.event.stopPropagation();
if (f) { if (f) {
f(...args); f(...args);
} }
@ -847,72 +842,42 @@ export class Message extends React.PureComponent<Props, State> {
); );
return ( return (
<ContextMenu <Menu
id={triggerId} id={triggerId}
onShow={onContextMenuShown} onShown={onContextMenuShown}
onHide={onContextMenuHidden} onHidden={onContextMenuHidden}
animation={animation.fade}
> >
{!multipleAttachments && attachments && attachments[0] ? ( {!multipleAttachments && attachments && attachments[0] ? (
<MenuItem <Item
attributes={{ onClick={(e: any) => {
className: 'module-message__context__download', e.event.stopPropagation();
}}
onClick={(e: Event) => {
e.stopPropagation();
if (onDownload) { if (onDownload) {
onDownload(isDangerous); onDownload(isDangerous);
} }
}} }}
> >
{window.i18n('downloadAttachment')} {window.i18n('downloadAttachment')}
</MenuItem> </Item>
) : null} ) : null}
<MenuItem onClick={wrap(onCopyText)}> <Item onClick={wrap(onCopyText)}>{window.i18n('copyMessage')}</Item>
{window.i18n('copyMessage')} <Item onClick={this.onReplyPrivate}>
</MenuItem>
<MenuItem
attributes={{
className: 'module-message__context__reply',
}}
onClick={this.onReplyPrivate}
>
{window.i18n('replyToMessage')} {window.i18n('replyToMessage')}
</MenuItem> </Item>
<MenuItem <Item onClick={wrap(onShowDetail)}>
attributes={{
className: 'module-message__context__more-info',
}}
onClick={wrap(onShowDetail)}
>
{window.i18n('moreInformation')} {window.i18n('moreInformation')}
</MenuItem> </Item>
{showRetry ? ( {showRetry ? (
<MenuItem <Item onClick={wrap(onRetrySend)}>{window.i18n('resend')}</Item>
attributes={{
className: 'module-message__context__retry-send',
}}
onClick={wrap(onRetrySend)}
>
{window.i18n('resend')}
</MenuItem>
) : null} ) : null}
{isDeletable ? ( {isDeletable ? (
<MenuItem <Item onClick={wrap(onDelete)}>{deleteMessageCtxText}</Item>
attributes={{
className: 'module-message__context__delete-message',
}}
onClick={wrap(onDelete)}
>
{deleteMessageCtxText}
</MenuItem>
) : null} ) : null}
{isModerator && isPublic ? ( {isModerator && isPublic ? (
<MenuItem onClick={wrap(onBanUser)}> <Item onClick={wrap(onBanUser)}>{window.i18n('banUser')}</Item>
{window.i18n('banUser')}
</MenuItem>
) : null} ) : null}
</ContextMenu> </Menu>
); );
} }
@ -1036,7 +1001,7 @@ export class Message extends React.PureComponent<Props, State> {
return ( return (
<div id={id} className={classNames(divClasses)}> <div id={id} className={classNames(divClasses)}>
<ContextMenuTrigger id={rightClickTriggerId}> <MenuProvider id={rightClickTriggerId}>
{this.renderAvatar()} {this.renderAvatar()}
<div <div
className={classNames( className={classNames(
@ -1105,7 +1070,7 @@ export class Message extends React.PureComponent<Props, State> {
? this.renderContextMenu(rightClickTriggerId) ? this.renderContextMenu(rightClickTriggerId)
: null} : null}
</div> </div>
</ContextMenuTrigger> </MenuProvider>
</div> </div>
); );
} }
@ -1147,8 +1112,8 @@ export class Message extends React.PureComponent<Props, State> {
); );
} }
private onReplyPrivate(e: Event) { private onReplyPrivate(e: any) {
e.stopPropagation(); e.event.stopPropagation();
if (this.props && this.props.onReply) { if (this.props && this.props.onReply) {
this.props.onReply(this.props.timestamp); this.props.onReply(this.props.timestamp);
} }

@ -6,6 +6,7 @@ import { PropsData as ConversationListItemPropsType } from '../ConversationListI
import { createOrUpdateItem, getItemById } from '../../../js/modules/data'; import { createOrUpdateItem, getItemById } from '../../../js/modules/data';
import { APPLY_THEME } from '../../state/ducks/theme'; import { APPLY_THEME } from '../../state/ducks/theme';
import { darkTheme, lightTheme } from '../../state/ducks/SessionTheme'; import { darkTheme, lightTheme } from '../../state/ducks/SessionTheme';
// tslint:disable-next-line: no-import-side-effect no-submodule-imports
export enum SectionType { export enum SectionType {
Profile, Profile,

@ -1,4 +1,4 @@
import styled, { css } from 'styled-components'; import styled from 'styled-components';
export interface FlexProps { export interface FlexProps {
children?: any; children?: any;

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { animation, Item, Menu, MenuProvider } from 'react-contexify';
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon'; import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
interface Props { interface Props {
searchString: string; searchString: string;
@ -21,7 +21,7 @@ export class SessionSearchInput extends React.Component<Props> {
return ( return (
<> <>
<ContextMenuTrigger id={triggerId}> <MenuProvider id={triggerId}>
<div className="session-search-input"> <div className="session-search-input">
<SessionIconButton <SessionIconButton
iconSize={SessionIconSize.Medium} iconSize={SessionIconSize.Medium}
@ -34,28 +34,28 @@ export class SessionSearchInput extends React.Component<Props> {
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
/> />
</div> </div>
</ContextMenuTrigger> </MenuProvider>
<ContextMenu id={triggerId}> <Menu id={triggerId} animation={animation.fade}>
<MenuItem onClick={() => document.execCommand('undo')}> <Item onClick={() => document.execCommand('undo')}>
{window.i18n('editMenuUndo')} {window.i18n('editMenuUndo')}
</MenuItem> </Item>
<MenuItem onClick={() => document.execCommand('redo')}> <Item onClick={() => document.execCommand('redo')}>
{window.i18n('editMenuRedo')} {window.i18n('editMenuRedo')}
</MenuItem> </Item>
<hr /> <hr />
<MenuItem onClick={() => document.execCommand('cut')}> <Item onClick={() => document.execCommand('cut')}>
{window.i18n('editMenuCut')} {window.i18n('editMenuCut')}
</MenuItem> </Item>
<MenuItem onClick={() => document.execCommand('copy')}> <Item onClick={() => document.execCommand('copy')}>
{window.i18n('editMenuCopy')} {window.i18n('editMenuCopy')}
</MenuItem> </Item>
<MenuItem onClick={() => document.execCommand('paste')}> <Item onClick={() => document.execCommand('paste')}>
{window.i18n('editMenuPaste')} {window.i18n('editMenuPaste')}
</MenuItem> </Item>
<MenuItem onClick={() => document.execCommand('selectAll')}> <Item onClick={() => document.execCommand('selectAll')}>
{window.i18n('editMenuSelectAll')} {window.i18n('editMenuSelectAll')}
</MenuItem> </Item>
</ContextMenu> </Menu>
</> </>
); );
} }

@ -5,14 +5,13 @@ import { Props, SessionIcon } from '../icon';
import { SessionNotificationCount } from '../SessionNotificationCount'; import { SessionNotificationCount } from '../SessionNotificationCount';
interface SProps extends Props { interface SProps extends Props {
onClick: any; onClick?: any;
notificationCount?: number; notificationCount?: number;
isSelected: boolean; isSelected: boolean;
} }
export class SessionIconButton extends React.PureComponent<SProps> { export class SessionIconButton extends React.PureComponent<SProps> {
public static readonly extendedDefaults = { public static readonly extendedDefaults = {
onClick: () => null,
notificationCount: undefined, notificationCount: undefined,
isSelected: false, isSelected: false,
}; };

@ -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<TimerOption>;
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 (
<Menu id={triggerId} animation={animation.fade}>
{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
)}
</Menu>
);
};

@ -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 (
<Menu id={triggerId} animation={animation.fade}>
{getBlockMenuItem(
isMe,
type === 'direct',
isBlocked,
onBlockContact,
onUnblockContact,
window.i18n
)}
{/* {!isPublic && !isRss && !isMe ? (
<Item onClick={onChangeNickname}>
{i18n('changeNickname')}
</Item>
) : 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
)}
</Menu>
);
};

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { MenuItem, SubMenu } from 'react-contextmenu'; import { LocalizerType } from '../../../types/Util';
import { LocalizerType } from '../../types/Util'; import { TimerOption } from '../../conversation/ConversationHeader';
import { TimerOption } from '../../components/conversation/ConversationHeader'; import { Item, Submenu } from 'react-contexify';
function showTimerOptions( function showTimerOptions(
isPublic: boolean, isPublic: boolean,
@ -116,7 +116,7 @@ export function getInviteContactMenuItem(
i18n: LocalizerType i18n: LocalizerType
): JSX.Element | null { ): JSX.Element | null {
if (showInviteContact(Boolean(isGroup), Boolean(isPublic))) { if (showInviteContact(Boolean(isGroup), Boolean(isPublic))) {
return <MenuItem onClick={action}>{i18n('inviteContacts')}</MenuItem>; return <Item onClick={action}>{i18n('inviteContacts')}</Item>;
} }
return null; return null;
} }
@ -140,9 +140,9 @@ export function getDeleteContactMenuItem(
) )
) { ) {
if (isPublic) { if (isPublic) {
return <MenuItem onClick={action}>{i18n('leaveGroup')}</MenuItem>; return <Item onClick={action}>{i18n('leaveGroup')}</Item>;
} }
return <MenuItem onClick={action}>{i18n('delete')}</MenuItem>; return <Item onClick={action}>{i18n('delete')}</Item>;
} }
return null; return null;
} }
@ -163,7 +163,7 @@ export function getLeaveGroupMenuItem(
Boolean(isRss) Boolean(isRss)
) )
) { ) {
return <MenuItem onClick={action}>{i18n('leaveGroup')}</MenuItem>; return <Item onClick={action}>{i18n('leaveGroup')}</Item>;
} }
return null; return null;
} }
@ -175,7 +175,7 @@ export function getUpdateGroupNameMenuItem(
i18n: LocalizerType i18n: LocalizerType
): JSX.Element | null { ): JSX.Element | null {
if (showUpdateGroupName(Boolean(amMod), Boolean(isKickedFromGroup))) { if (showUpdateGroupName(Boolean(amMod), Boolean(isKickedFromGroup))) {
return <MenuItem onClick={action}>{i18n('editGroup')}</MenuItem>; return <Item onClick={action}>{i18n('editGroup')}</Item>;
} }
return null; return null;
} }
@ -187,7 +187,7 @@ export function getRemoveModeratorsMenuItem(
i18n: LocalizerType i18n: LocalizerType
): JSX.Element | null { ): JSX.Element | null {
if (showRemoveModerators(Boolean(amMod), Boolean(isKickedFromGroup))) { if (showRemoveModerators(Boolean(amMod), Boolean(isKickedFromGroup))) {
return <MenuItem onClick={action}>{i18n('removeModerators')}</MenuItem>; return <Item onClick={action}>{i18n('removeModerators')}</Item>;
} }
return null; return null;
} }
@ -199,7 +199,7 @@ export function getAddModeratorsMenuItem(
i18n: LocalizerType i18n: LocalizerType
): JSX.Element | null { ): JSX.Element | null {
if (showAddModerators(Boolean(amMod), Boolean(isKickedFromGroup))) { if (showAddModerators(Boolean(amMod), Boolean(isKickedFromGroup))) {
return <MenuItem onClick={action}>{i18n('addModerators')}</MenuItem>; return <Item onClick={action}>{i18n('addModerators')}</Item>;
} }
return null; return null;
} }
@ -213,7 +213,7 @@ export function getCopyMenuItem(
): JSX.Element | null { ): JSX.Element | null {
if (showCopyId(Boolean(isPublic), Boolean(isRss), Boolean(isGroup))) { if (showCopyId(Boolean(isPublic), Boolean(isRss), Boolean(isGroup))) {
const copyIdLabel = i18n('copySessionID'); const copyIdLabel = i18n('copySessionID');
return <MenuItem onClick={action}>{copyIdLabel}</MenuItem>; return <Item onClick={action}>{copyIdLabel}</Item>;
} }
return null; return null;
} }
@ -238,21 +238,21 @@ export function getDisappearingMenuItem(
const isRtlMode = isRtlBody(); const isRtlMode = isRtlBody();
return ( return (
// Remove the && false to make context menu work with RTL support // Remove the && false to make context menu work with RTL support
<SubMenu <Submenu
title={i18n('disappearingMessages') as any} label={i18n('disappearingMessages') as any}
rtl={isRtlMode && false} // rtl={isRtlMode && false}
> >
{(timerOptions || []).map(item => ( {(timerOptions || []).map(item => (
<MenuItem <Item
key={item.value} key={item.value}
onClick={() => { onClick={() => {
action(item.value); action(item.value);
}} }}
> >
{item.name} {item.name}
</MenuItem> </Item>
))} ))}
</SubMenu> </Submenu>
); );
} }
return null; return null;
@ -270,7 +270,7 @@ export function getShowMemberMenuItem(
i18n: LocalizerType i18n: LocalizerType
): JSX.Element | null { ): JSX.Element | null {
if (showMemberMenu(Boolean(isPublic), Boolean(isRss), Boolean(isGroup))) { if (showMemberMenu(Boolean(isPublic), Boolean(isRss), Boolean(isGroup))) {
return <MenuItem onClick={action}>{i18n('groupMembers')}</MenuItem>; return <Item onClick={action}>{i18n('groupMembers')}</Item>;
} }
return null; return null;
} }
@ -291,7 +291,7 @@ export function getShowSafetyNumberMenuItem(
Boolean(isMe) Boolean(isMe)
) )
) { ) {
return <MenuItem onClick={action}>{i18n('showSafetyNumber')}</MenuItem>; return <Item onClick={action}>{i18n('showSafetyNumber')}</Item>;
} }
return null; return null;
} }
@ -312,7 +312,7 @@ export function getResetSessionMenuItem(
Boolean(isBlocked) Boolean(isBlocked)
) )
) { ) {
return <MenuItem onClick={action}>{i18n('resetSession')}</MenuItem>; return <Item onClick={action}>{i18n('resetSession')}</Item>;
} }
return null; return null;
} }
@ -328,7 +328,7 @@ export function getBlockMenuItem(
if (showBlock(Boolean(isMe), Boolean(isPrivate))) { if (showBlock(Boolean(isMe), Boolean(isPrivate))) {
const blockTitle = isBlocked ? i18n('unblockUser') : i18n('blockUser'); const blockTitle = isBlocked ? i18n('unblockUser') : i18n('blockUser');
const blockHandler = isBlocked ? actionUnblock : actionBlock; const blockHandler = isBlocked ? actionUnblock : actionBlock;
return <MenuItem onClick={blockHandler}>{blockTitle}</MenuItem>; return <Item onClick={blockHandler}>{blockTitle}</Item>;
} }
return null; return null;
} }
@ -349,7 +349,7 @@ export function getClearNicknameMenuItem(
Boolean(hasNickname) Boolean(hasNickname)
) )
) { ) {
return <MenuItem onClick={action}>{i18n('clearNickname')}</MenuItem>; return <Item onClick={action}>{i18n('clearNickname')}</Item>;
} }
return null; return null;
} }
@ -360,7 +360,7 @@ export function getDeleteMessagesMenuItem(
i18n: LocalizerType i18n: LocalizerType
): JSX.Element | null { ): JSX.Element | null {
if (showDeleteMessages(Boolean(isPublic))) { if (showDeleteMessages(Boolean(isPublic))) {
return <MenuItem onClick={action}>{i18n('deleteMessages')}</MenuItem>; return <Item onClick={action}>{i18n('deleteMessages')}</Item>;
} }
return null; return null;
} }

@ -5,7 +5,7 @@ import * as StringUtils from './String';
import * as NumberUtils from './Number'; import * as NumberUtils from './Number';
import * as PromiseUtils from './Promise'; import * as PromiseUtils from './Promise';
import * as ProtobufUtils from './Protobuf'; import * as ProtobufUtils from './Protobuf';
import * as MenuUtils from './Menu'; import * as MenuUtils from '../../components/session/menu/Menu';
import * as ToastUtils from './Toast'; import * as ToastUtils from './Toast';
export * from './Attachments'; export * from './Attachments';

Loading…
Cancel
Save