diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c4df15896..37f51cd4e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -240,8 +240,8 @@ "multipleJoinedTheGroup": "$name$ joined the group.", "kickedFromTheGroup": "$name$ was removed from the group.", "multipleKickedFromTheGroup": "$name$ were removed from the group.", - "blockUser": "Block", - "unblockUser": "Unblock", + "block": "Block", + "unblock": "Unblock", "unblocked": "Unblocked", "blocked": "Blocked", "blockedSettingsTitle": "Blocked Contacts", diff --git a/tools/afterCrowdInFetch.sh b/tools/afterCrowdInFetch.sh index 9f4c31572..f03a31979 100755 --- a/tools/afterCrowdInFetch.sh +++ b/tools/afterCrowdInFetch.sh @@ -14,5 +14,5 @@ mv $PWD/_locales/zh-TW $PWD/_locales/zh_TW echo 'Updated locales from crowdin to session-desktop folder' -python $PWD/tools/updateI18nKeysType.py +python2 $PWD/tools/updateI18nKeysType.py diff --git a/ts/components/SplitViewContainer.tsx b/ts/components/SplitViewContainer.tsx index cdca3d3ff..6ba7a7041 100644 --- a/ts/components/SplitViewContainer.tsx +++ b/ts/components/SplitViewContainer.tsx @@ -67,11 +67,7 @@ const TopSplitViewPanel = ({ const MIN_HEIGHT_TOP = 200; const MIN_HEIGHT_BOTTOM = 0; -export const SplitViewContainer: React.FunctionComponent = ({ - disableTop, - top, - bottom, -}) => { +export const SplitViewContainer = ({ disableTop, top, bottom }: SplitViewProps) => { const [topHeight, setTopHeight] = useState(undefined); const [separatorYPosition, setSeparatorYPosition] = useState(undefined); const [dragging, setDragging] = useState(false); diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 736cff2be..53806eb51 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { Avatar, AvatarSize } from '../avatar/Avatar'; import { contextMenu } from 'react-contexify'; +import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import { ConversationNotificationSettingType } from '../../models/conversationAttributes'; import { @@ -11,8 +12,13 @@ import { isMessageSelectionMode, isRightPanelShowing, } from '../../state/selectors/conversations'; -import { useDispatch, useSelector } from 'react-redux'; +import { + useConversationUsername, + useExpireTimer, + useIsKickedFromGroup, +} from '../../hooks/useParamSelector'; +import { callRecipient } from '../../interactions/conversationInteractions'; import { deleteMessagesById, deleteMessagesByIdForEveryone, @@ -23,37 +29,31 @@ import { openRightPanel, resetSelectedMessageIds, } from '../../state/ducks/conversations'; -import { callRecipient } from '../../interactions/conversationInteractions'; import { getHasIncomingCall, getHasOngoingCall } from '../../state/selectors/call'; -import { - useConversationUsername, - useExpireTimer, - useIsKickedFromGroup, - useIsRequest, -} from '../../hooks/useParamSelector'; -import { - SessionButton, - SessionButtonColor, - SessionButtonShape, - SessionButtonType, -} from '../basic/SessionButton'; -import { SessionIconButton } from '../icon'; -import { ConversationHeaderMenu } from '../menu/ConversationHeaderMenu'; -import { Flex } from '../basic/Flex'; -import { ExpirationTimerOptions } from '../../util/expiringMessages'; import { useSelectedConversationKey, useSelectedIsActive, useSelectedIsBlocked, + useSelectedIsPrivateFriend, useSelectedIsGroup, useSelectedIsKickedFromGroup, - useSelectedisNoteToSelf, useSelectedIsPrivate, useSelectedIsPublic, useSelectedMembers, useSelectedNotificationSetting, useSelectedSubscriberCount, + useSelectedisNoteToSelf, } from '../../state/selectors/selectedConversation'; +import { ExpirationTimerOptions } from '../../util/expiringMessages'; +import { Flex } from '../basic/Flex'; +import { + SessionButton, + SessionButtonColor, + SessionButtonShape, + SessionButtonType, +} from '../basic/SessionButton'; +import { SessionIconButton } from '../icon'; +import { ConversationHeaderMenu } from '../menu/ConversationHeaderMenu'; export interface TimerOption { name: string; @@ -123,9 +123,15 @@ const TripleDotContainer = styled.div` const TripleDotsMenu = (props: { triggerId: string; showBackButton: boolean }) => { const { showBackButton } = props; + + const isPrivateFriend = useSelectedIsPrivateFriend(); + const isPrivate = useSelectedIsPrivate(); if (showBackButton) { return null; } + if (isPrivate && !isPrivateFriend) { + return null; + } return ( { const hasOngoingCall = useSelector(getHasOngoingCall); const canCall = !(hasIncomingCall || hasOngoingCall); - const isRequest = useIsRequest(selectedConvoKey); + const isPrivateFriend = useSelectedIsPrivateFriend(); - if (!isPrivate || isMe || !selectedConvoKey || isBlocked || !isActive || isRequest) { + if ( + !isPrivate || + isMe || + !selectedConvoKey || + isBlocked || + !isActive || + !isPrivateFriend // call requires us to be friends + ) { return null; } diff --git a/ts/components/conversation/ConversationRequestButtons.tsx b/ts/components/conversation/MessageRequestButtons.tsx similarity index 57% rename from ts/components/conversation/ConversationRequestButtons.tsx rename to ts/components/conversation/MessageRequestButtons.tsx index 3d6d05e5a..aec060261 100644 --- a/ts/components/conversation/ConversationRequestButtons.tsx +++ b/ts/components/conversation/MessageRequestButtons.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; -import { useIsRequest } from '../../hooks/useParamSelector'; +import { useIsIncomingRequest } from '../../hooks/useParamSelector'; import { approveConvoAndSendResponse, declineConversationWithConfirm, @@ -10,13 +10,7 @@ import { getConversationController } from '../../session/conversations'; import { hasSelectedConversationIncomingMessages } from '../../state/selectors/conversations'; import { useSelectedConversationKey } from '../../state/selectors/selectedConversation'; import { SessionButton, SessionButtonColor } from '../basic/SessionButton'; - -const handleAcceptConversationRequest = async (convoId: string) => { - const convo = getConversationController().get(convoId); - await convo.setDidApproveMe(true); - await convo.addOutgoingApprovalMessage(Date.now()); - await approveConvoAndSendResponse(convoId, true); -}; +import { ConversationRequestExplanation } from './SubtleNotification'; const ConversationRequestBanner = styled.div` display: flex; @@ -24,6 +18,7 @@ const ConversationRequestBanner = styled.div` justify-content: center; padding: var(--margins-lg); gap: var(--margins-lg); + background-color: var(--background-secondary-color); `; const ConversationBannerRow = styled.div` @@ -37,22 +32,72 @@ const ConversationBannerRow = styled.div` } `; +const StyledBlockUserText = styled.span` + color: var(--danger-color); + cursor: pointer; + font-size: var(--font-size-md); + align-self: center; + font-weight: 700; +`; + +const handleDeclineConversationRequest = (convoId: string, currentSelected: string | undefined) => { + declineConversationWithConfirm({ + conversationId: convoId, + syncToDevices: true, + blockContact: false, + currentlySelectedConvo: currentSelected, + }); +}; + +const handleDeclineAndBlockConversationRequest = ( + convoId: string, + currentSelected: string | undefined +) => { + declineConversationWithConfirm({ + conversationId: convoId, + syncToDevices: true, + blockContact: true, + currentlySelectedConvo: currentSelected, + }); +}; + +const handleAcceptConversationRequest = async (convoId: string) => { + const convo = getConversationController().get(convoId); + if (!convo) return; + await convo.setDidApproveMe(true, false); + await convo.setIsApproved(true, false); + await convo.commit(); + await convo.addOutgoingApprovalMessage(Date.now()); + await approveConvoAndSendResponse(convoId, true); +}; + export const ConversationMessageRequestButtons = () => { const selectedConvoId = useSelectedConversationKey(); const hasIncomingMessages = useSelector(hasSelectedConversationIncomingMessages); - const isIncomingMessageRequest = useIsRequest(selectedConvoId); + const isIncomingRequest = useIsIncomingRequest(selectedConvoId); if (!selectedConvoId || !hasIncomingMessages) { return null; } - if (!isIncomingMessageRequest) { + if (!isIncomingRequest) { return null; } return ( + { + handleDeclineAndBlockConversationRequest(selectedConvoId, selectedConvoId); + }} + data-testid="decline-and-block-message-request" + > + {window.i18n('block')} + + + + { @@ -65,7 +110,7 @@ export const ConversationMessageRequestButtons = () => { buttonColor={SessionButtonColor.Danger} text={window.i18n('decline')} onClick={() => { - declineConversationWithConfirm(selectedConvoId, true); + handleDeclineConversationRequest(selectedConvoId, selectedConvoId); }} dataTestId="decline-message-request" /> diff --git a/ts/components/conversation/SessionConversation.tsx b/ts/components/conversation/SessionConversation.tsx index 1e77c835e..67ca5919f 100644 --- a/ts/components/conversation/SessionConversation.tsx +++ b/ts/components/conversation/SessionConversation.tsx @@ -1,8 +1,8 @@ -import React from 'react'; import _ from 'lodash'; +import React from 'react'; -import classNames from 'classnames'; import autoBind from 'auto-bind'; +import classNames from 'classnames'; import { CompositionBox, @@ -18,49 +18,45 @@ import { SessionMessagesListContainer } from './SessionMessagesListContainer'; import { SessionFileDropzone } from './SessionFileDropzone'; -import { InConversationCallContainer } from '../calling/InConversationCallContainer'; -import { SplitViewContainer } from '../SplitViewContainer'; -import { LightboxGallery, MediaItemType } from '../lightbox/LightboxGallery'; +import { blobToArrayBuffer } from 'blob-util'; +import loadImage from 'blueimp-load-image'; import { Data } from '../../data/data'; +import { markAllReadByConvoId } from '../../interactions/conversationInteractions'; +import { MAX_ATTACHMENT_FILESIZE_BYTES } from '../../session/constants'; import { getConversationController } from '../../session/conversations'; import { ToastUtils } from '../../session/utils'; import { + ReduxConversationType, + SortedMessageModelProps, openConversationToSpecificMessage, quoteMessage, - ReduxConversationType, resetSelectedMessageIds, - SortedMessageModelProps, updateMentionsMembers, } from '../../state/ducks/conversations'; import { updateConfirmModal } from '../../state/ducks/modalDialog'; -import { SessionTheme } from '../../themes/SessionTheme'; import { addStagedAttachmentsInConversation } from '../../state/ducks/stagedAttachments'; +import { SessionTheme } from '../../themes/SessionTheme'; import { MIME } from '../../types'; import { AttachmentTypeWithPath } from '../../types/Attachment'; -import { arrayBufferToObjectURL, AttachmentUtil, GoogleChrome } from '../../util'; -import { SessionButtonColor } from '../basic/SessionButton'; -import { MessageView } from '../MainViewController'; -import { ConversationHeaderWithDetails } from './ConversationHeader'; -import { MessageDetail } from './message/message-item/MessageDetail'; -import { SessionRightPanelWithDetails } from './SessionRightPanel'; import { + THUMBNAIL_CONTENT_TYPE, makeImageThumbnailBuffer, makeVideoScreenshot, - THUMBNAIL_CONTENT_TYPE, } from '../../types/attachments/VisualAttachment'; -import { blobToArrayBuffer } from 'blob-util'; -import { MAX_ATTACHMENT_FILESIZE_BYTES } from '../../session/constants'; -import { ConversationMessageRequestButtons } from './ConversationRequestButtons'; -import { - NoMessageNoMessageInConversation, - RespondToMessageRequestWarning, -} from './SubtleNotification'; +import { AttachmentUtil, GoogleChrome, arrayBufferToObjectURL } from '../../util'; import { getCurrentRecoveryPhrase } from '../../util/storage'; -import loadImage from 'blueimp-load-image'; -import { markAllReadByConvoId } from '../../interactions/conversationInteractions'; +import { MessageView } from '../MainViewController'; +import { SplitViewContainer } from '../SplitViewContainer'; +import { SessionButtonColor } from '../basic/SessionButton'; +import { InConversationCallContainer } from '../calling/InConversationCallContainer'; +import { LightboxGallery, MediaItemType } from '../lightbox/LightboxGallery'; +import { ConversationHeaderWithDetails } from './ConversationHeader'; +import { SessionRightPanelWithDetails } from './SessionRightPanel'; +import { NoMessageNoMessageInConversation } from './SubtleNotification'; +import { MessageDetail } from './message/message-item/MessageDetail'; -import { SessionSpinner } from '../basic/SessionSpinner'; import styled from 'styled-components'; +import { SessionSpinner } from '../basic/SessionSpinner'; // tslint:disable: jsx-curly-spacing interface State { @@ -276,7 +272,7 @@ export class SessionConversation extends React.Component {
- + } bottom={ @@ -289,7 +285,6 @@ export class SessionConversation extends React.Component { /> {isDraggingFile && }
- ; }; - export const messageContainerDomID = 'messages-container'; export type ScrollToLoadedReasons = @@ -127,6 +127,7 @@ class SessionMessagesListContainerInner extends React.Component { isTyping={!!conversation.isTyping} key="typing-bubble" /> + { +export const ConversationRequestExplanation = () => { const selectedConversation = useSelectedConversationKey(); - const isIncomingMessageRequest = useIsRequest(selectedConversation); + const isIncomingMessageRequest = useIsIncomingRequest(selectedConversation); const showMsgRequestUI = selectedConversation && isIncomingMessageRequest; const hasIncomingMessages = useSelector(hasSelectedConversationIncomingMessages); diff --git a/ts/components/leftpane/MessageRequestsBanner.tsx b/ts/components/leftpane/MessageRequestsBanner.tsx index 494f6c04b..ece893518 100644 --- a/ts/components/leftpane/MessageRequestsBanner.tsx +++ b/ts/components/leftpane/MessageRequestsBanner.tsx @@ -6,7 +6,7 @@ import styled from 'styled-components'; import { getUnreadConversationRequests } from '../../state/selectors/conversations'; import { getHideMessageRequestBanner } from '../../state/selectors/userConfig'; import { SessionIcon, SessionIconSize, SessionIconType } from '../icon'; -import { MemoMessageRequestBannerContextMenu } from '../menu/MessageRequestBannerContextMenu'; +import { MessageRequestBannerContextMenu } from '../menu/MessageRequestBannerContextMenu'; const StyledMessageRequestBanner = styled.div` height: 64px; @@ -126,7 +126,7 @@ export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { - + ); diff --git a/ts/components/leftpane/overlay/OverlayMessageRequest.tsx b/ts/components/leftpane/overlay/OverlayMessageRequest.tsx index cb3f09163..5d9bd695a 100644 --- a/ts/components/leftpane/overlay/OverlayMessageRequest.tsx +++ b/ts/components/leftpane/overlay/OverlayMessageRequest.tsx @@ -1,23 +1,19 @@ -import React from 'react'; +import React, { useEffect } from 'react'; // tslint:disable: no-submodule-imports use-simple-attributes -import { SpacerLG } from '../../basic/Text'; import { useDispatch, useSelector } from 'react-redux'; -import { getConversationRequests } from '../../../state/selectors/conversations'; -import { MemoConversationListItemWithDetails } from '../conversation-list-item/ConversationListItem'; +import useKey from 'react-use/lib/useKey'; import styled from 'styled-components'; -import { SessionButton, SessionButtonColor } from '../../basic/SessionButton'; -import { resetOverlayMode, SectionType, showLeftPaneSection } from '../../../state/ducks/section'; -import { getConversationController } from '../../../session/conversations'; +import { declineConversationWithoutConfirm } from '../../../interactions/conversationInteractions'; import { forceSyncConfigurationNowIfNeeded } from '../../../session/utils/sync/syncUtils'; -import { BlockedNumberController } from '../../../util'; -import useKey from 'react-use/lib/useKey'; -import { - ReduxConversationType, - resetConversationExternal, -} from '../../../state/ducks/conversations'; +import { resetConversationExternal } from '../../../state/ducks/conversations'; import { updateConfirmModal } from '../../../state/ducks/modalDialog'; +import { SectionType, resetOverlayMode, showLeftPaneSection } from '../../../state/ducks/section'; +import { getConversationRequests } from '../../../state/selectors/conversations'; import { useSelectedConversationKey } from '../../../state/selectors/selectedConversation'; +import { SessionButton, SessionButtonColor } from '../../basic/SessionButton'; +import { SpacerLG } from '../../basic/Text'; +import { MemoConversationListItemWithDetails } from '../conversation-list-item/ConversationListItem'; const MessageRequestListPlaceholder = styled.div` color: var(--conversation-tab-text-color); @@ -51,9 +47,20 @@ export const OverlayMessageRequest = () => { function closeOverlay() { dispatch(resetOverlayMode()); } + + const currentlySelectedConvo = useSelectedConversationKey(); const convoRequestCount = useSelector(getConversationRequests).length; const messageRequests = useSelector(getConversationRequests); - const selectedConvoId = useSelectedConversationKey(); + + useEffect(() => { + // if no more requests, return to placeholder screen + + if (convoRequestCount === 0) { + dispatch(resetOverlayMode()); + dispatch(showLeftPaneSection(SectionType.Message)); + dispatch(resetConversationExternal()); + } + }, [dispatch, convoRequestCount]); const buttonText = window.i18n('clearAll'); @@ -61,7 +68,7 @@ export const OverlayMessageRequest = () => { * Blocks all message request conversations and synchronizes across linked devices * @returns void */ - function handleClearAllRequestsClick(convoRequests: Array) { + function handleClearAllRequestsClick() { const { i18n } = window; const title = i18n('clearAllConfirmationTitle'); const message = i18n('clearAllConfirmationBody'); @@ -73,42 +80,23 @@ export const OverlayMessageRequest = () => { message, onClose, onClickOk: async () => { - window?.log?.info('Blocking all conversations'); - if (!convoRequests) { + window?.log?.info('Blocking all message requests'); + if (!messageRequests) { window?.log?.info('No conversation requests to block.'); return; } - let newConvosBlocked = []; - const convoController = getConversationController(); - await Promise.all( - (newConvosBlocked = convoRequests.filter(async convo => { - const { id } = convo; - const convoModel = convoController.get(id); - if (!convoModel.isBlocked()) { - await BlockedNumberController.block(id); - await convoModel.commit(); - } - await convoModel.setIsApproved(false); - - // if we're looking at the convo to decline, close the convo - if (selectedConvoId === id) { - dispatch(resetConversationExternal()); - } - return true; - })) - ); - - if (newConvosBlocked) { - await forceSyncConfigurationNowIfNeeded(); + for (let index = 0; index < messageRequests.length; index++) { + const convo = messageRequests[index]; + await declineConversationWithoutConfirm({ + blockContact: false, + conversationId: convo.id, + currentlySelectedConvo, + syncToDevices: false, + }); } - // if no more requests, return to placeholder screen - if (convoRequestCount === newConvosBlocked.length) { - dispatch(resetOverlayMode()); - dispatch(showLeftPaneSection(SectionType.Message)); - dispatch(resetConversationExternal()); - } + await forceSyncConfigurationNowIfNeeded(); }, }) ); @@ -123,9 +111,7 @@ export const OverlayMessageRequest = () => { { - handleClearAllRequestsClick(messageRequests); - }} + onClick={handleClearAllRequestsClick} /> ) : ( diff --git a/ts/components/menu/ConversationHeaderMenu.tsx b/ts/components/menu/ConversationHeaderMenu.tsx index adbb2212f..170c42c0b 100644 --- a/ts/components/menu/ConversationHeaderMenu.tsx +++ b/ts/components/menu/ConversationHeaderMenu.tsx @@ -1,31 +1,45 @@ import React from 'react'; -import { animation, Menu } from 'react-contexify'; +import { animation, Item, Menu, Submenu } from 'react-contexify'; +import { useSelector } from 'react-redux'; +import { + setDisappearingMessagesByConvoId, + setNotificationForConvoId, +} from '../../interactions/conversationInteractions'; +import { + ConversationNotificationSetting, + ConversationNotificationSettingType, +} from '../../models/conversationAttributes'; +import { + useSelectedConversationKey, + useSelectedIsActive, + useSelectedIsBlocked, + useSelectedIsKickedFromGroup, + useSelectedIsLeft, + useSelectedIsPrivate, + useSelectedIsPrivateFriend, + useSelectedIsPublic, + useSelectedNotificationSetting, +} from '../../state/selectors/selectedConversation'; +import { getTimerOptions } from '../../state/selectors/timerOptions'; +import { LocalizerKeys } from '../../types/LocalizerKeys'; +import { ContextConversationId } from '../leftpane/conversation-list-item/ConversationListItem'; +import { SessionContextMenuContainer } from '../SessionContextMenuContainer'; import { - AcceptMenuItem, AddModeratorsMenuItem, BanMenuItem, BlockMenuItem, ChangeNicknameMenuItem, ClearNicknameMenuItem, - CopyMenuItem, - DeclineMenuItem, DeleteContactMenuItem, DeleteMessagesMenuItem, - DisappearingMessageMenuItem, InviteContactMenuItem, LeaveGroupMenuItem, MarkAllReadMenuItem, - NotificationForConvoMenuItem, - PinConversationMenuItem, RemoveModeratorsMenuItem, ShowUserDetailsMenuItem, UnbanMenuItem, UpdateGroupNameMenuItem, } from './Menu'; -import _ from 'lodash'; -import { ContextConversationId } from '../leftpane/conversation-list-item/ConversationListItem'; -import { SessionContextMenuContainer } from '../SessionContextMenuContainer'; -import { useSelectedConversationKey } from '../../state/selectors/selectedConversation'; export type PropsConversationHeaderMenu = { triggerId: string; @@ -34,23 +48,26 @@ export type PropsConversationHeaderMenu = { export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { const { triggerId } = props; - const selectedConversation = useSelectedConversationKey(); + const convoId = useSelectedConversationKey(); + const isPrivateFriend = useSelectedIsPrivateFriend(); + const isPrivate = useSelectedIsPrivate(); - if (!selectedConversation) { - throw new Error('selectedConversation must be set for a header to be visible!'); + if (!convoId) { + throw new Error('convoId must be set for a header to be visible!'); + } + + // we do not want the triple dots menu at all if this is not a friend at all + if (isPrivate && !isPrivateFriend) { + return null; } return ( - + - - - - @@ -69,3 +86,119 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { ); }; + +/** + * Only accessible through the triple dots menu on the conversation header. Not on the Conversation list item, because there is too much to check for before showing it + */ +const DisappearingMessageMenuItem = (): JSX.Element | null => { + const selectedConvoId = useSelectedConversationKey(); + const isBlocked = useSelectedIsBlocked(); + const isActive = useSelectedIsActive(); + const isPublic = useSelectedIsPublic(); + const isLeft = useSelectedIsLeft(); + const isKickedFromGroup = useSelectedIsKickedFromGroup(); + const timerOptions = useSelector(getTimerOptions).timerOptions; + const isFriend = useSelectedIsPrivateFriend(); + const isPrivate = useSelectedIsPrivate(); + + if ( + !selectedConvoId || + isPublic || + isLeft || + isKickedFromGroup || + isBlocked || + !isActive || + (isPrivate && !isFriend) + ) { + return null; + } + + // const isRtlMode = isRtlBody(); + + return ( + // Remove the && false to make context menu work with RTL support + + {timerOptions.map(item => ( + { + await setDisappearingMessagesByConvoId(selectedConvoId, item.value); + }} + > + {item.name} + + ))} + + ); +}; + +/** + * Only accessible through the triple dots menu on the conversation header. Not on the Conversation list item, because there is too much to check for before showing it + */ +const NotificationForConvoMenuItem = (): JSX.Element | null => { + const selectedConvoId = useSelectedConversationKey(); + + const currentNotificationSetting = useSelectedNotificationSetting(); + const isBlocked = useSelectedIsBlocked(); + const isActive = useSelectedIsActive(); + const isLeft = useSelectedIsLeft(); + const isKickedFromGroup = useSelectedIsKickedFromGroup(); + const isFriend = useSelectedIsPrivateFriend(); + const isPrivate = useSelectedIsPrivate(); + + if ( + !selectedConvoId || + isLeft || + isKickedFromGroup || + isBlocked || + !isActive || + (isPrivate && !isFriend) + ) { + return null; + } + + // 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 + + {(notificationForConvoOptions || []).map(item => { + const disabled = item.value === currentNotificationSetting; + + return ( + { + await setNotificationForConvoId(selectedConvoId, item.value); + }} + disabled={disabled} + > + {item.name} + + ); + })} + + ); + + return null; +}; diff --git a/ts/components/menu/ConversationListItemContextMenu.tsx b/ts/components/menu/ConversationListItemContextMenu.tsx index 29b1a01c0..5d37c90f4 100644 --- a/ts/components/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/menu/ConversationListItemContextMenu.tsx @@ -3,20 +3,20 @@ import { animation, Menu } from 'react-contexify'; import _ from 'lodash'; import { - AcceptMenuItem, + AcceptMsgRequestMenuItem, BanMenuItem, BlockMenuItem, ChangeNicknameMenuItem, ClearNicknameMenuItem, CopyMenuItem, - DeclineMenuItem, + DeclineAndBlockMsgRequestMenuItem, + DeclineMsgRequestMenuItem, DeleteContactMenuItem, DeleteMessagesMenuItem, InviteContactMenuItem, LeaveGroupMenuItem, MarkAllReadMenuItem, MarkConversationUnreadMenuItem, - NotificationForConvoMenuItem, PinConversationMenuItem, ShowUserDetailsMenuItem, UnbanMenuItem, @@ -33,9 +33,9 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) => return ( - - - + + + diff --git a/ts/components/menu/Menu.tsx b/ts/components/menu/Menu.tsx index cc989ffa9..6512a6828 100644 --- a/ts/components/menu/Menu.tsx +++ b/ts/components/menu/Menu.tsx @@ -1,22 +1,21 @@ import React, { useContext } from 'react'; -import { Item, Submenu } from 'react-contexify'; +import { Item } from 'react-contexify'; import { useDispatch, useSelector } from 'react-redux'; import { useAvatarPath, useConversationUsername, useHasNickname, - useIsActive, useIsBlinded, useIsBlocked, + useIsIncomingRequest, useIsKickedFromGroup, useIsLeft, useIsMe, useIsPinned, useIsPrivate, + useIsPrivateAndFriend, useIsPublic, - useIsRequest, - useNotificationSetting, useWeAreAdmin, } from '../../hooks/useParamSelector'; import { @@ -27,8 +26,6 @@ import { declineConversationWithConfirm, deleteAllMessagesByConvoIdWithConfirmation, markAllReadByConvoId, - setDisappearingMessagesByConvoId, - setNotificationForConvoId, showAddModeratorsByConvoId, showBanUserByConvoId, showInviteContactByConvoId, @@ -38,65 +35,20 @@ import { 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 { hideMessageRequestBanner } from '../../state/ducks/userConfig'; import { getIsMessageSection } from '../../state/selectors/section'; -import { getTimerOptions } from '../../state/selectors/timerOptions'; -import { LocalizerKeys } from '../../types/LocalizerKeys'; +import { + useSelectedConversationKey, + useSelectedIsPrivateFriend, +} from '../../state/selectors/selectedConversation'; 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, @@ -108,30 +60,6 @@ function showDeleteContact( 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, @@ -176,7 +104,7 @@ export const InviteContactMenuItem = (): JSX.Element | null => { export const PinConversationMenuItem = (): JSX.Element | null => { const conversationId = useContext(ContextConversationId); const isMessagesSection = useSelector(getIsMessageSection); - const isRequest = useIsRequest(conversationId); + const isRequest = useIsIncomingRequest(conversationId); const isPinned = useIsPinned(conversationId); if (isMessagesSection && !isRequest) { @@ -195,7 +123,7 @@ export const PinConversationMenuItem = (): JSX.Element | null => { export const MarkConversationUnreadMenuItem = (): JSX.Element | null => { const conversationId = useContext(ContextConversationId); const isMessagesSection = useSelector(getIsMessageSection); - const isRequest = useIsRequest(conversationId); + const isRequest = useIsIncomingRequest(conversationId); if (isMessagesSection && !isRequest) { const conversation = getConversationController().get(conversationId); @@ -216,7 +144,7 @@ export const DeleteContactMenuItem = () => { const isLeft = useIsLeft(convoId); const isKickedFromGroup = useIsKickedFromGroup(convoId); const isPrivate = useIsPrivate(convoId); - const isRequest = useIsRequest(convoId); + const isRequest = useIsIncomingRequest(convoId); if (showDeleteContact(!isPrivate, isPublic, isLeft, isKickedFromGroup, isRequest)) { let menuItemText: string; @@ -330,7 +258,7 @@ export const RemoveModeratorsMenuItem = (): JSX.Element | null => { const isKickedFromGroup = useIsKickedFromGroup(convoId); const weAreAdmin = useWeAreAdmin(convoId); - if (showRemoveModerators(weAreAdmin, Boolean(isPublic), Boolean(isKickedFromGroup))) { + if (!isKickedFromGroup && weAreAdmin && isPublic) { return ( { @@ -350,7 +278,7 @@ export const AddModeratorsMenuItem = (): JSX.Element | null => { const isKickedFromGroup = useIsKickedFromGroup(convoId); const weAreAdmin = useWeAreAdmin(convoId); - if (showAddModerators(weAreAdmin, isPublic, isKickedFromGroup)) { + if (!isKickedFromGroup && weAreAdmin && isPublic) { return ( { @@ -370,7 +298,7 @@ export const UnbanMenuItem = (): JSX.Element | null => { const isKickedFromGroup = useIsKickedFromGroup(convoId); const weAreAdmin = useWeAreAdmin(convoId); - if (showUnbanUser(weAreAdmin, isPublic, isKickedFromGroup)) { + if (isPublic && !isKickedFromGroup && weAreAdmin) { return ( { @@ -390,7 +318,7 @@ export const BanMenuItem = (): JSX.Element | null => { const isKickedFromGroup = useIsKickedFromGroup(convoId); const weAreAdmin = useWeAreAdmin(convoId); - if (showBanUser(weAreAdmin, isPublic, isKickedFromGroup)) { + if (isPublic && !isKickedFromGroup && weAreAdmin) { return ( { @@ -410,7 +338,9 @@ export const CopyMenuItem = (): JSX.Element | null => { const isPrivate = useIsPrivate(convoId); const isBlinded = useIsBlinded(convoId); - if (showCopyId(isPublic, isPrivate, isBlinded)) { + // we want to show the copyId for open groups and private chats only + + if ((isPrivate && !isBlinded) || isPublic) { const copyIdLabel = isPublic ? window.i18n('copyOpenGroupURL') : window.i18n('copySessionID'); return ( { export const MarkAllReadMenuItem = (): JSX.Element | null => { const convoId = useContext(ContextConversationId); - const isRequest = useIsRequest(convoId); - if (!isRequest) { + const isIncomingRequest = useIsIncomingRequest(convoId); + if (!isIncomingRequest) { return ( markAllReadByConvoId(convoId)}>{window.i18n('markAllAsRead')} ); @@ -437,105 +367,6 @@ export const MarkAllReadMenuItem = (): JSX.Element | 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 - - {timerOptions.map(item => ( - { - await setDisappearingMessagesByConvoId(convoId, item.value); - }} - > - {item.name} - - ))} - - ); - } - 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 - - {(notificationForConvoOptions || []).map(item => { - const disabled = item.value === currentNotificationSetting; - - return ( - { - await setNotificationForConvoId(convoId, item.value); - }} - disabled={disabled} - > - {item.name} - - ); - })} - - ); - } - return null; -}; - export function isRtlBody(): boolean { const body = document.getElementsByTagName('body').item(0); @@ -547,10 +378,10 @@ export const BlockMenuItem = (): JSX.Element | null => { const isMe = useIsMe(convoId); const isBlocked = useIsBlocked(convoId); const isPrivate = useIsPrivate(convoId); - const isRequest = useIsRequest(convoId); + const isIncomingRequest = useIsIncomingRequest(convoId); - if (showBlock(Boolean(isMe), Boolean(isPrivate), Boolean(isRequest))) { - const blockTitle = isBlocked ? window.i18n('unblockUser') : window.i18n('blockUser'); + if (!isMe && isPrivate && !isIncomingRequest) { + const blockTitle = isBlocked ? window.i18n('unblock') : window.i18n('block'); const blockHandler = isBlocked ? () => unblockConvoById(convoId) : () => blockConvoById(convoId); @@ -564,75 +395,63 @@ export const ClearNicknameMenuItem = (): JSX.Element | null => { const isMe = useIsMe(convoId); const hasNickname = useHasNickname(convoId); const isPrivate = useIsPrivate(convoId); - const isRequest = Boolean(useIsRequest(convoId)); // easier to copy paste + const isPrivateAndFriend = useIsPrivateAndFriend(convoId); - if (showClearNickname(Boolean(isMe), Boolean(hasNickname), Boolean(isPrivate), isRequest)) { - return ( - clearNickNameByConvoId(convoId)}>{window.i18n('clearNickname')} - ); + if (isMe || !hasNickname || !isPrivate || !isPrivateAndFriend) { + return null; } - return null; + + return ( + clearNickNameByConvoId(convoId)}>{window.i18n('clearNickname')} + ); }; export const ChangeNicknameMenuItem = () => { const convoId = useContext(ContextConversationId); const isMe = useIsMe(convoId); const isPrivate = useIsPrivate(convoId); - const isRequest = useIsRequest(convoId); - + const isPrivateAndFriend = useSelectedIsPrivateFriend(); const dispatch = useDispatch(); - if (showChangeNickname(isMe, isPrivate, isRequest)) { - return ( - { - dispatch(changeNickNameModal({ conversationId: convoId })); - }} - > - {window.i18n('changeNickname')} - - ); - } - return null; -}; - -export const DeleteMessagesMenuItem = () => { - const convoId = useContext(ContextConversationId); - const isRequest = useIsRequest(convoId); - if (isRequest) { + if (isMe || !isPrivate || !isPrivateAndFriend) { return null; } - return ( { - deleteAllMessagesByConvoIdWithConfirmation(convoId); + dispatch(changeNickNameModal({ conversationId: convoId })); }} > - {window.i18n('deleteMessages')} + {window.i18n('changeNickname')} ); }; -export const HideBannerMenuItem = (): JSX.Element => { - const dispatch = useDispatch(); +export const DeleteMessagesMenuItem = () => { + const convoId = useContext(ContextConversationId); + + if (!convoId) { + return null; + } + return ( { - dispatch(hideMessageRequestBanner()); + deleteAllMessagesByConvoIdWithConfirmation(convoId); }} > - {window.i18n('hideBanner')} + {window.i18n('deleteMessages')} ); }; -export const AcceptMenuItem = () => { +export const AcceptMsgRequestMenuItem = () => { const convoId = useContext(ContextConversationId); - const isRequest = useIsRequest(convoId); + const isRequest = useIsIncomingRequest(convoId); const convo = getConversationController().get(convoId); + const isPrivate = useIsPrivate(convoId); - if (isRequest) { + if (isRequest && isPrivate) { return ( { @@ -648,15 +467,22 @@ export const AcceptMenuItem = () => { return null; }; -export const DeclineMenuItem = () => { +export const DeclineMsgRequestMenuItem = () => { const convoId = useContext(ContextConversationId); - const isRequest = useIsRequest(convoId); + const isRequest = useIsIncomingRequest(convoId); + const isPrivate = useIsPrivate(convoId); + const selected = useSelectedConversationKey(); - if (isRequest) { + if (isPrivate && isRequest) { return ( { - declineConversationWithConfirm(convoId, true); + declineConversationWithConfirm({ + conversationId: convoId, + syncToDevices: true, + blockContact: false, + currentlySelectedConvo: selected || undefined, + }); }} > {window.i18n('decline')} @@ -665,3 +491,28 @@ export const DeclineMenuItem = () => { } return null; }; + +export const DeclineAndBlockMsgRequestMenuItem = () => { + const convoId = useContext(ContextConversationId); + const isRequest = useIsIncomingRequest(convoId); + const selected = useSelectedConversationKey(); + const isPrivate = useIsPrivate(convoId); + + if (isRequest && isPrivate) { + return ( + { + declineConversationWithConfirm({ + conversationId: convoId, + syncToDevices: true, + blockContact: true, + currentlySelectedConvo: selected || undefined, + }); + }} + > + {window.i18n('block')} + + ); + } + return null; +}; diff --git a/ts/components/menu/MessageRequestBannerContextMenu.tsx b/ts/components/menu/MessageRequestBannerContextMenu.tsx index d7c3de2de..273e50c4c 100644 --- a/ts/components/menu/MessageRequestBannerContextMenu.tsx +++ b/ts/components/menu/MessageRequestBannerContextMenu.tsx @@ -2,14 +2,29 @@ import React from 'react'; import { animation, Menu } from 'react-contexify'; import _ from 'lodash'; -import { HideBannerMenuItem } from './Menu'; import { SessionContextMenuContainer } from '../SessionContextMenuContainer'; +import { useDispatch } from 'react-redux'; +import { hideMessageRequestBanner } from '../../state/ducks/userConfig'; +import { Item } from 'react-contexify'; export type PropsContextConversationItem = { triggerId: string; }; -const MessageRequestBannerContextMenu = (props: PropsContextConversationItem) => { +const HideBannerMenuItem = (): JSX.Element => { + const dispatch = useDispatch(); + return ( + { + dispatch(hideMessageRequestBanner()); + }} + > + {window.i18n('hideBanner')} + + ); +}; + +export const MessageRequestBannerContextMenu = (props: PropsContextConversationItem) => { const { triggerId } = props; return ( @@ -20,11 +35,3 @@ const MessageRequestBannerContextMenu = (props: PropsContextConversationItem) => ); }; - -function propsAreEqual(prev: PropsContextConversationItem, next: PropsContextConversationItem) { - return _.isEqual(prev, next); -} -export const MemoMessageRequestBannerContextMenu = React.memo( - MessageRequestBannerContextMenu, - propsAreEqual -); diff --git a/ts/components/settings/BlockedList.tsx b/ts/components/settings/BlockedList.tsx index 4dd0a59aa..b9083d7ca 100644 --- a/ts/components/settings/BlockedList.tsx +++ b/ts/components/settings/BlockedList.tsx @@ -134,7 +134,7 @@ export const BlockedContactsList = () => { {hasAtLeastOneSelected && expanded ? ( diff --git a/ts/hooks/useParamSelector.ts b/ts/hooks/useParamSelector.ts index a9a95bf9c..ecfc62c6c 100644 --- a/ts/hooks/useParamSelector.ts +++ b/ts/hooks/useParamSelector.ts @@ -1,10 +1,14 @@ -import { isEmpty, isNumber, pick } from 'lodash'; +import { isEmpty, isNumber } from 'lodash'; import { useSelector } from 'react-redux'; -import { ConversationModel } from '../models/conversation'; +import { + hasValidIncomingRequestValues, + hasValidOutgoingRequestValues, +} from '../models/conversation'; import { PubKey } from '../session/types'; import { UserUtils } from '../session/utils'; import { StateType } from '../state/reducer'; import { getMessageReactsProps } from '../state/selectors/conversations'; +import { isPrivateAndFriend } from '../state/selectors/selectedConversation'; export function useAvatarPath(convoId: string | undefined) { const convoProps = useConversationPropsById(convoId); @@ -79,6 +83,18 @@ export function useIsPrivate(convoId?: string) { return Boolean(convoProps && convoProps.isPrivate); } +export function useIsPrivateAndFriend(convoId?: string) { + const convoProps = useConversationPropsById(convoId); + if (!convoProps) { + return false; + } + return isPrivateAndFriend({ + approvedMe: convoProps.didApproveMe || false, + isApproved: convoProps.isApproved || false, + isPrivate: convoProps.isPrivate || false, + }); +} + export function useIsBlinded(convoId?: string) { if (!convoId) { return false; @@ -154,16 +170,39 @@ export function useIsApproved(convoId?: string) { return Boolean(convoProps && convoProps.isApproved); } -export function useIsRequest(convoId?: string) { +export function useIsIncomingRequest(convoId?: string) { + const convoProps = useConversationPropsById(convoId); + if (!convoProps) { + return false; + } + return Boolean( + convoProps && + hasValidIncomingRequestValues({ + isMe: convoProps.isMe || false, + isApproved: convoProps.isApproved || false, + isPrivate: convoProps.isPrivate || false, + isBlocked: convoProps.isBlocked || false, + didApproveMe: convoProps.didApproveMe || false, + activeAt: convoProps.activeAt || 0, + }) + ); +} + +export function useIsOutgoingRequest(convoId?: string) { const convoProps = useConversationPropsById(convoId); if (!convoProps) { return false; } return Boolean( convoProps && - ConversationModel.hasValidIncomingRequestValues( - pick(convoProps, ['isMe', 'isApproved', 'isPrivate', 'isBlocked', 'activeAt']) - ) + hasValidOutgoingRequestValues({ + isMe: convoProps.isMe || false, + isApproved: convoProps.isApproved || false, + didApproveMe: convoProps.didApproveMe || false, + isPrivate: convoProps.isPrivate || false, + isBlocked: convoProps.isBlocked || false, + activeAt: convoProps.activeAt || 0, + }) ); } diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 733776440..32a3e90cc 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -41,6 +41,7 @@ import { setLastProfileUpdateTimestamp } from '../util/storage'; import { OpenGroupUtils } from '../session/apis/open_group_api/utils'; import { SessionUtilUserGroups } from '../session/utils/libsession/libsession_utils_user_groups'; import { leaveClosedGroup } from '../session/group/closed-group'; +import { SessionUtilContact } from '../session/utils/libsession/libsession_utils_contacts'; export function copyPublicKeyByConvoId(convoId: string) { if (OpenGroupUtils.isOpenGroupV2(convoId)) { @@ -67,6 +68,11 @@ export async function blockConvoById(conversationId: string) { return; } + // I don't think we want to reset the approved fields when blocking a contact + // if (conversation.isPrivate()) { + // await conversation.setIsApproved(false); + // } + await BlockedNumberController.block(conversation.id); await conversation.commit(); ToastUtils.pushToastSuccess('blocked', window.i18n('blocked')); @@ -115,17 +121,76 @@ export const approveConvoAndSendResponse = async ( } }; -export const declineConversationWithConfirm = (convoId: string, syncToDevices: boolean = true) => { +export async function declineConversationWithoutConfirm({ + blockContact, + conversationId, + currentlySelectedConvo, + syncToDevices, +}: { + conversationId: string; + currentlySelectedConvo: string | undefined; + syncToDevices: boolean; + blockContact: boolean; // if set to false, the contact will just be set to not approved +}) { + const conversationToDecline = getConversationController().get(conversationId); + + if (!conversationToDecline || conversationToDecline.isApproved()) { + window?.log?.info('Conversation is already declined.'); + return; + } + + // we mark the conversation as inactive. This way it wont' show up in the UI. + // we cannot delete it completely on desktop, because we might need the convo details for sogs/group convos. + conversationToDecline.set('active_at', undefined); + await conversationToDecline.setIsApproved(false, false); + await conversationToDecline.setDidApproveMe(false, false); + // this will update the value in the wrapper if needed but not remove the entry if we want it gone. The remove is done below with removeContactFromWrapper + await conversationToDecline.commit(); + if (blockContact) { + await blockConvoById(conversationId); + } + if (window.sessionFeatureFlags.useSharedUtilForUserConfig) { + // when removing a message request, without blocking it, we actually have no need to store the conversation in the wrapper. So just remove the entry + + if ( + conversationToDecline.isPrivate() && + !SessionUtilContact.isContactToStoreInContactsWrapper(conversationToDecline) + ) { + await SessionUtilContact.removeContactFromWrapper(conversationToDecline.id); + } + } + + if (syncToDevices) { + await forceSyncConfigurationNowIfNeeded(); + } + if (currentlySelectedConvo && currentlySelectedConvo === conversationId) { + window?.inboxStore?.dispatch(resetConversationExternal()); + } +} + +export const declineConversationWithConfirm = ({ + conversationId, + syncToDevices, + blockContact, + currentlySelectedConvo, +}: { + conversationId: string; + currentlySelectedConvo: string | undefined; + syncToDevices: boolean; + blockContact: boolean; // if set to false, the contact will just be set to not approved +}) => { window?.inboxStore?.dispatch( updateConfirmModal({ - okText: window.i18n('decline'), + okText: blockContact ? window.i18n('block') : window.i18n('decline'), cancelText: window.i18n('cancel'), message: window.i18n('declineRequestMessage'), onClickOk: async () => { - await declineConversationWithoutConfirm(convoId, syncToDevices); - await blockConvoById(convoId); - await forceSyncConfigurationNowIfNeeded(); - window?.inboxStore?.dispatch(resetConversationExternal()); + await declineConversationWithoutConfirm({ + conversationId, + currentlySelectedConvo, + blockContact, + syncToDevices, + }); }, onClickCancel: () => { window?.inboxStore?.dispatch(updateConfirmModal(null)); @@ -137,28 +202,6 @@ export const declineConversationWithConfirm = (convoId: string, syncToDevices: b ); }; -/** - * Sets the approval fields to false for conversation. Does not send anything back. - */ -const declineConversationWithoutConfirm = async ( - conversationId: string, - syncToDevices: boolean = true -) => { - const conversationToDecline = getConversationController().get(conversationId); - - if (!conversationToDecline || conversationToDecline.isApproved()) { - window?.log?.info('Conversation is already declined.'); - return; - } - - await conversationToDecline.setIsApproved(false); - - // Conversation was not approved before so a sync is needed - if (syncToDevices) { - await forceSyncConfigurationNowIfNeeded(); - } -}; - export async function showUpdateGroupNameByConvoId(conversationId: string) { const conversation = getConversationController().get(conversationId); if (conversation.isClosedGroup()) { diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 152c5849f..372321c50 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -167,44 +167,6 @@ export class ConversationModel extends Backbone.Model { ); } - /** - * Method to evaluate if a convo contains the right values - * @param values Required properties to evaluate if this is a message request - */ - public static hasValidIncomingRequestValues({ - isMe, - isApproved, - isBlocked, - isPrivate, - activeAt, - }: { - isMe?: boolean; - isApproved?: boolean; - isBlocked?: boolean; - isPrivate?: boolean; - activeAt?: number; - }): boolean { - // if a convo is not active, it means we didn't get any messages nor sent any. - const isActive = activeAt && isFinite(activeAt) && activeAt > 0; - return Boolean(isPrivate && !isMe && !isApproved && !isBlocked && isActive); - } - - public static hasValidOutgoingRequestValues({ - isMe, - didApproveMe, - isApproved, - isBlocked, - isPrivate, - }: { - isMe?: boolean; - isApproved?: boolean; - didApproveMe?: boolean; - isBlocked?: boolean; - isPrivate?: boolean; - }): boolean { - return Boolean(!isMe && isApproved && isPrivate && !isBlocked && !didApproveMe); - } - public idForLogging() { if (this.isPrivate()) { return this.id; @@ -727,11 +689,13 @@ export class ConversationModel extends Backbone.Model { * Does this conversation contain the properties to be considered a message request */ public isIncomingRequest(): boolean { - return ConversationModel.hasValidIncomingRequestValues({ + return hasValidIncomingRequestValues({ isMe: this.isMe(), isApproved: this.isApproved(), isBlocked: this.isBlocked(), isPrivate: this.isPrivate(), + activeAt: this.get('active_at'), + didApproveMe: this.didApproveMe(), }); } @@ -739,12 +703,13 @@ export class ConversationModel extends Backbone.Model { * Is this conversation an outgoing message request */ public isOutgoingRequest(): boolean { - return ConversationModel.hasValidOutgoingRequestValues({ - isMe: this.isMe(), - isApproved: this.isApproved(), - didApproveMe: this.didApproveMe(), - isBlocked: this.isBlocked(), - isPrivate: this.isPrivate(), + return hasValidOutgoingRequestValues({ + isMe: this.isMe() || false, + isApproved: this.isApproved() || false, + didApproveMe: this.didApproveMe() || false, + isBlocked: this.isBlocked() || false, + isPrivate: this.isPrivate() || false, + activeAt: this.get('active_at') || 0, }); } @@ -2351,3 +2316,47 @@ export class ConversationCollection extends Backbone.Collection 0; + + return Boolean(!isMe && isApproved && isPrivate && !isBlocked && !didApproveMe && isActive); +} + +/** + * Method to evaluate if a convo contains the right values + * @param values Required properties to evaluate if this is a message request + */ +export function hasValidIncomingRequestValues({ + isMe, + isApproved, + isBlocked, + isPrivate, + activeAt, + didApproveMe, +}: { + isMe: boolean; + isApproved: boolean; + isBlocked: boolean; + isPrivate: boolean; + didApproveMe: boolean; + activeAt: number; +}): boolean { + // if a convo is not active, it means we didn't get any messages nor sent any. + const isActive = activeAt && isFinite(activeAt) && activeAt > 0; + return Boolean(isPrivate && !isMe && !isApproved && !isBlocked && isActive && didApproveMe); +} diff --git a/ts/session/utils/libsession/libsession_utils_contacts.ts b/ts/session/utils/libsession/libsession_utils_contacts.ts index 60a6c6173..730107060 100644 --- a/ts/session/utils/libsession/libsession_utils_contacts.ts +++ b/ts/session/utils/libsession/libsession_utils_contacts.ts @@ -113,6 +113,7 @@ async function insertContactFromDBIntoWrapperAndRefresh(id: string): Promise ): Array => { return filter(sortedConversations, conversation => { - const { isApproved, isBlocked, isPrivate, isMe, activeAt } = conversation; - const isRequest = ConversationModel.hasValidIncomingRequestValues({ - isApproved, - isBlocked, - isPrivate, - isMe, - activeAt, + const { isApproved, isBlocked, isPrivate, isMe, activeAt, didApproveMe } = conversation; + const isIncomingRequest = hasValidIncomingRequestValues({ + isApproved: isApproved || false, + isBlocked: isBlocked || false, + isPrivate: isPrivate || false, + isMe: isMe || false, + activeAt: activeAt || 0, + didApproveMe: didApproveMe || false, }); - return isRequest; + return isIncomingRequest; }); }; diff --git a/ts/state/selectors/selectedConversation.ts b/ts/state/selectors/selectedConversation.ts index 4ec411610..46ffb800f 100644 --- a/ts/state/selectors/selectedConversation.ts +++ b/ts/state/selectors/selectedConversation.ts @@ -33,6 +33,14 @@ const getIsSelectedBlocked = (state: StateType): boolean => { return Boolean(getSelectedConversation(state)?.isBlocked) || false; }; +const getSelectedIsApproved = (state: StateType): boolean => { + return Boolean(getSelectedConversation(state)?.isApproved) || false; +}; + +const getSelectedApprovedMe = (state: StateType): boolean => { + return Boolean(getSelectedConversation(state)?.didApproveMe) || false; +}; + /** * Returns true if the currently selected conversation is active (has an active_at field > 0) */ @@ -143,6 +151,39 @@ export function useSelectedIsBlocked() { return useSelector(getIsSelectedBlocked); } +export function useSelectedIsApproved() { + return useSelector(getSelectedIsApproved); +} + +export function useSelectedApprovedMe() { + return useSelector(getSelectedApprovedMe); +} + +/** + * Returns true if the given arguments corresponds to a private contact which is approved both sides. i.e. a friend. + */ +export function isPrivateAndFriend({ + approvedMe, + isApproved, + isPrivate, +}: { + isPrivate: boolean; + isApproved: boolean; + approvedMe: boolean; +}) { + return isPrivate && isApproved && approvedMe; +} + +/** + * Returns true if the selected conversation is private and is approved both sides + */ +export function useSelectedIsPrivateFriend() { + const isPrivate = useSelectedIsPrivate(); + const isApproved = useSelectedIsApproved(); + const approvedMe = useSelectedApprovedMe(); + return isPrivateAndFriend({ isPrivate, isApproved, approvedMe }); +} + export function useSelectedIsActive() { return useSelector(getIsSelectedActive); } diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index f928a0a32..d9f249462 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -240,8 +240,8 @@ export type LocalizerKeys = | 'multipleJoinedTheGroup' | 'kickedFromTheGroup' | 'multipleKickedFromTheGroup' - | 'blockUser' - | 'unblockUser' + | 'block' + | 'unblock' | 'unblocked' | 'blocked' | 'blockedSettingsTitle'