diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 0c5f2da59..18dc9e7f6 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -478,5 +478,6 @@ "youHaveANewFriendRequest": "You have a new friend request", "clearAllConfirmationTitle": "Clear All Message Requests", "clearAllConfirmationBody": "Are you sure you want to clear all message requests?", + "hideBanner": "Hide", "openMessageRequestInboxDescription": "View your Message Request inbox" } diff --git a/ts/components/conversation/ConversationRequestButtons.tsx b/ts/components/conversation/ConversationRequestButtons.tsx index 14fc4287b..5bfee92b0 100644 --- a/ts/components/conversation/ConversationRequestButtons.tsx +++ b/ts/components/conversation/ConversationRequestButtons.tsx @@ -1,22 +1,17 @@ import React, { useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import styled from 'styled-components'; import { getMessageCountByType } from '../../data/data'; import { approveConvoAndSendResponse, - blockConvoById, - declineConversation, + declineConversationWithConfirm, } from '../../interactions/conversationInteractions'; import { MessageDirection } from '../../models/messageType'; import { getConversationController } from '../../session/conversations'; -import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/syncUtils'; -import { clearConversationFocus } from '../../state/ducks/conversations'; -import { updateConfirmModal } from '../../state/ducks/modalDialog'; import { getSelectedConversation } from '../../state/selectors/conversations'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; export const ConversationMessageRequestButtons = () => { - const dispatch = useDispatch(); const selectedConversation = useSelector(getSelectedConversation); const [hasIncoming, setHasIncomingMsgs] = useState(false); const [incomingChecked, setIncomingChecked] = useState(false); @@ -50,26 +45,7 @@ export const ConversationMessageRequestButtons = () => { .isRequest(); const handleDeclineConversationRequest = () => { - dispatch( - updateConfirmModal({ - okText: window.i18n('decline'), - cancelText: window.i18n('cancel'), - message: window.i18n('declineRequestMessage'), - onClickOk: async () => { - const { id } = selectedConversation; - await declineConversation(id, false); - await blockConvoById(id); - await forceSyncConfigurationNowIfNeeded(); - clearConversationFocus(); - }, - onClickCancel: () => { - dispatch(updateConfirmModal(null)); - }, - onClickClose: () => { - dispatch(updateConfirmModal(null)); - }, - }) - ); + declineConversationWithConfirm(selectedConversation.id, true); }; const handleAcceptConversationRequest = async () => { diff --git a/ts/components/leftpane/MessageRequestsBanner.tsx b/ts/components/leftpane/MessageRequestsBanner.tsx index 284f2bb2d..cc8c8dd53 100644 --- a/ts/components/leftpane/MessageRequestsBanner.tsx +++ b/ts/components/leftpane/MessageRequestsBanner.tsx @@ -1,9 +1,12 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { contextMenu } from 'react-contexify'; +import { createPortal } from 'react-dom'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; import { getConversationRequests } from '../../state/selectors/conversations'; import { getHideMessageRequestBanner } from '../../state/selectors/userConfig'; import { SessionIcon, SessionIconSize, SessionIconType } from '../icon'; +import { MemoMessageRequestBannerContextMenu } from '../menu/MessageRequestBannerContextMenu'; const StyledMessageRequestBanner = styled.div` height: 64px; @@ -86,13 +89,38 @@ export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { const { handleOnClick } = props; const conversationRequests = useSelector(getConversationRequests); const hideRequestBanner = useSelector(getHideMessageRequestBanner); + const [isMenuOpen, setIsMenuOpen] = useState(false); if (!conversationRequests.length || hideRequestBanner) { return null; } + const triggerId = 'msg-req-banner'; + + const handleOnContextMenu = (e: any) => { + contextMenu.show({ + id: triggerId, + event: e, + }); + + setIsMenuOpen(true); + }; + + const handleOnClickBanner = (e: React.MouseEvent) => { + if (e.button === 0 && !isMenuOpen) { + handleOnClick(); + } + }; + return ( - + { + e.stopPropagation(); + e.preventDefault(); + }} + > {window.i18n('messageRequests')} @@ -100,6 +128,13 @@ export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => {
{conversationRequests.length || 0}
+ + +
); }; + +const Portal = ({ children }: { children: any }) => { + return createPortal(children, document.querySelector('.inbox.index') as Element); +}; diff --git a/ts/components/menu/ConversationListItemContextMenu.tsx b/ts/components/menu/ConversationListItemContextMenu.tsx index 0aaef25c0..75b02bce1 100644 --- a/ts/components/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/menu/ConversationListItemContextMenu.tsx @@ -3,11 +3,13 @@ import { animation, Menu } from 'react-contexify'; import _ from 'lodash'; import { + AcceptMenuItem, BanMenuItem, BlockMenuItem, ChangeNicknameMenuItem, ClearNicknameMenuItem, CopyMenuItem, + DeclineMenuItem, DeleteContactMenuItem, DeleteMessagesMenuItem, InviteContactMenuItem, @@ -28,6 +30,8 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) => return ( + + diff --git a/ts/components/menu/Menu.tsx b/ts/components/menu/Menu.tsx index b80335b50..6002ae14c 100644 --- a/ts/components/menu/Menu.tsx +++ b/ts/components/menu/Menu.tsx @@ -16,9 +16,11 @@ import { useWeAreAdmin, } from '../../hooks/useParamSelector'; import { + approveConvoAndSendResponse, blockConvoById, clearNickNameByConvoId, copyPublicKeyByConvoId, + declineConversationWithConfirm, deleteAllMessagesByConvoIdWithConfirmation, markAllReadByConvoId, setDisappearingMessagesByConvoId, @@ -44,6 +46,7 @@ import { updateUserDetailsModal, } from '../../state/ducks/modalDialog'; import { SectionType } from '../../state/ducks/section'; +import { hideMessageRequestBanner } from '../../state/ducks/userConfig'; import { getNumberOfPinnedConversations } from '../../state/selectors/conversations'; import { getFocusedSection } from '../../state/selectors/section'; import { getTimerOptions } from '../../state/selectors/timerOptions'; @@ -558,3 +561,57 @@ export const DeleteMessagesMenuItem = () => { ); }; + +export const HideBannerMenuItem = (): JSX.Element => { + const dispatch = useDispatch(); + return ( + { + dispatch(hideMessageRequestBanner()); + }} + > + {window.i18n('hideBanner')} + + ); +}; + +export const AcceptMenuItem = () => { + const convoId = useContext(ContextConversationId); + const convo = getConversationController().get(convoId); + const showMenuItem = convo.isRequest(); + + if (showMenuItem) { + return ( + { + await convo.setDidApproveMe(true); + await convo.addOutgoingApprovalMessage(Date.now()); + await approveConvoAndSendResponse(convoId, true); + }} + > + {window.i18n('accept')} + + ); + } + return null; +}; + +export const DeclineMenuItem = () => { + const convoId = useContext(ContextConversationId); + const showMenuItem = getConversationController() + .get(convoId) + .isRequest(); + + if (showMenuItem) { + return ( + { + declineConversationWithConfirm(convoId, true); + }} + > + {window.i18n('decline')} + + ); + } + return null; +}; diff --git a/ts/components/menu/MessageRequestBannerContextMenu.tsx b/ts/components/menu/MessageRequestBannerContextMenu.tsx new file mode 100644 index 000000000..672d07088 --- /dev/null +++ b/ts/components/menu/MessageRequestBannerContextMenu.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { animation, Menu } from 'react-contexify'; +import _ from 'lodash'; + +import { HideBannerMenuItem } from './Menu'; + +export type PropsContextConversationItem = { + triggerId: string; +}; + +const MessageRequestBannerContextMenu = (props: PropsContextConversationItem) => { + const { triggerId } = props; + + return ( + + + + ); +}; + +function propsAreEqual(prev: PropsContextConversationItem, next: PropsContextConversationItem) { + return _.isEqual(prev, next); +} +export const MemoMessageRequestBannerContextMenu = React.memo( + MessageRequestBannerContextMenu, + propsAreEqual +); diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 51f4de058..c21cb4447 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -29,7 +29,11 @@ import { lastAvatarUploadTimestamp, removeAllMessagesInConversation, } from '../data/data'; -import { conversationReset, quoteMessage } from '../state/ducks/conversations'; +import { + clearConversationFocus, + conversationReset, + quoteMessage, +} from '../state/ducks/conversations'; import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager'; import { IMAGE_JPEG } from '../types/MIME'; import { FSv2 } from '../session/apis/file_server_api'; @@ -145,10 +149,32 @@ export const approveConvoAndSendResponse = async ( } }; +export const declineConversationWithConfirm = (convoId: string, syncToDevices: boolean = true) => { + window?.inboxStore?.dispatch( + updateConfirmModal({ + okText: window.i18n('decline'), + cancelText: window.i18n('cancel'), + message: window.i18n('declineRequestMessage'), + onClickOk: async () => { + await declineConversationWithoutConfirm(convoId, syncToDevices); + await blockConvoById(convoId); + await forceSyncConfigurationNowIfNeeded(); + clearConversationFocus(); + }, + onClickCancel: () => { + window?.inboxStore?.dispatch(updateConfirmModal(null)); + }, + onClickClose: () => { + window?.inboxStore?.dispatch(updateConfirmModal(null)); + }, + }) + ); +}; + /** * Sets the approval fields to false for conversation. Sends decline message. */ -export const declineConversation = async ( +export const declineConversationWithoutConfirm = async ( conversationId: string, syncToDevices: boolean = true ) => { diff --git a/ts/state/ducks/userConfig.tsx b/ts/state/ducks/userConfig.tsx index da2974879..7881ffa62 100644 --- a/ts/state/ducks/userConfig.tsx +++ b/ts/state/ducks/userConfig.tsx @@ -32,6 +32,9 @@ const userConfigSlice = createSlice({ showMessageRequestBanner: state => { state.hideMessageRequests = false; }, + hideMessageRequestBanner: state => { + state.hideMessageRequests = true; + }, }, }); @@ -41,5 +44,6 @@ export const { disableRecoveryPhrasePrompt, toggleMessageRequests, showMessageRequestBanner, + hideMessageRequestBanner, } = actions; export const userConfigReducer = reducer; diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 996b6d5b4..a67688259 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -481,4 +481,5 @@ export type LocalizerKeys = | 'youHaveANewFriendRequest' | 'clearAllConfirmationTitle' | 'clearAllConfirmationBody' + | 'hideBanner' | 'reportIssue';