feat: add block/decline logic to msg request & wrapper

pull/2620/head
Audric Ackermann 2 years ago
parent ef6d9f1d51
commit faeb95fefd

@ -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",

@ -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

@ -67,11 +67,7 @@ const TopSplitViewPanel = ({
const MIN_HEIGHT_TOP = 200;
const MIN_HEIGHT_BOTTOM = 0;
export const SplitViewContainer: React.FunctionComponent<SplitViewProps> = ({
disableTop,
top,
bottom,
}) => {
export const SplitViewContainer = ({ disableTop, top, bottom }: SplitViewProps) => {
const [topHeight, setTopHeight] = useState<undefined | number>(undefined);
const [separatorYPosition, setSeparatorYPosition] = useState<undefined | number>(undefined);
const [dragging, setDragging] = useState(false);

@ -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 (
<TripleDotContainer
role="button"
@ -214,9 +220,16 @@ const CallButton = () => {
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;
}

@ -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 (
<ConversationRequestBanner>
<StyledBlockUserText
onClick={() => {
handleDeclineAndBlockConversationRequest(selectedConvoId, selectedConvoId);
}}
data-testid="decline-and-block-message-request"
>
{window.i18n('block')}
</StyledBlockUserText>
<ConversationRequestExplanation />
<ConversationBannerRow>
<SessionButton
onClick={async () => {
@ -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"
/>

@ -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<Props, State> {
<div className="conversation-messages">
<NoMessageNoMessageInConversation />
<ConversationMessageRequestButtons />
<SplitViewContainer
top={<InConversationCallContainer />}
bottom={
@ -289,7 +285,6 @@ export class SessionConversation extends React.Component<Props, State> {
/>
{isDraggingFile && <SessionFileDropzone />}
</div>
<RespondToMessageRequestWarning />
<CompositionBox
sendMessage={this.sendMessageFn}

@ -1,12 +1,12 @@
import React from 'react';
import { SessionScrollButton } from '../SessionScrollButton';
import { contextMenu } from 'react-contexify';
import { SessionScrollButton } from '../SessionScrollButton';
import { connect } from 'react-redux';
import { SessionMessagesList } from './SessionMessagesList';
import autoBind from 'auto-bind';
import styled from 'styled-components';
import {
quotedMessageToAnimate,
ReduxConversationType,
@ -19,17 +19,17 @@ import {
getQuotedMessageToAnimate,
getSortedMessagesOfSelectedConversation,
} from '../../state/selectors/conversations';
import { TypingBubble } from './TypingBubble';
import styled from 'styled-components';
import {
getSelectedConversation,
getSelectedConversationKey,
} from '../../state/selectors/selectedConversation';
import { SessionMessagesList } from './SessionMessagesList';
import { TypingBubble } from './TypingBubble';
import { ConversationMessageRequestButtons } from './MessageRequestButtons';
export type SessionMessageListProps = {
messageContainerRef: React.RefObject<HTMLDivElement>;
};
export const messageContainerDomID = 'messages-container';
export type ScrollToLoadedReasons =
@ -127,6 +127,7 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
isTyping={!!conversation.isTyping}
key="typing-bubble"
/>
<ConversationMessageRequestButtons />
<ScrollToLoadedMessageContext.Provider value={this.scrollToLoadedMessage}>
<SessionMessagesList

@ -1,7 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { useIsRequest } from '../../hooks/useParamSelector';
import {
getSelectedHasMessages,
hasSelectedConversationIncomingMessages,
@ -14,6 +13,7 @@ import {
} from '../../state/selectors/selectedConversation';
import { LocalizerKeys } from '../../types/LocalizerKeys';
import { SessionHtmlRenderer } from '../basic/SessionHTMLRenderer';
import { useIsIncomingRequest } from '../../hooks/useParamSelector';
const Container = styled.div`
display: flex;
@ -33,9 +33,9 @@ const TextInner = styled.div`
* This component is used to display a warning when the user is responding to a message request.
*
*/
export const RespondToMessageRequestWarning = () => {
export const ConversationRequestExplanation = () => {
const selectedConversation = useSelectedConversationKey();
const isIncomingMessageRequest = useIsRequest(selectedConversation);
const isIncomingMessageRequest = useIsIncomingRequest(selectedConversation);
const showMsgRequestUI = selectedConversation && isIncomingMessageRequest;
const hasIncomingMessages = useSelector(hasSelectedConversationIncomingMessages);

@ -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 }) => {
</StyledUnreadCounter>
</StyledMessageRequestBanner>
<Portal>
<MemoMessageRequestBannerContextMenu triggerId={triggerId} />
<MessageRequestBannerContextMenu triggerId={triggerId} />
</Portal>
</>
);

@ -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<ReduxConversationType>) {
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 = () => {
<SessionButton
buttonColor={SessionButtonColor.Danger}
text={buttonText}
onClick={() => {
handleClearAllRequestsClick(messageRequests);
}}
onClick={handleClearAllRequestsClick}
/>
</>
) : (

@ -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 (
<ContextConversationId.Provider value={selectedConversation}>
<ContextConversationId.Provider value={convoId}>
<SessionContextMenuContainer>
<Menu id={triggerId} animation={animation.fade}>
<AcceptMenuItem />
<DeclineMenuItem />
<DisappearingMessageMenuItem />
<NotificationForConvoMenuItem />
<PinConversationMenuItem />
<BlockMenuItem />
<CopyMenuItem />
<MarkAllReadMenuItem />
<ChangeNicknameMenuItem />
<ClearNicknameMenuItem />
@ -69,3 +86,119 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
</ContextConversationId.Provider>
);
};
/**
* 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
<Submenu
label={window.i18n('disappearingMessages')}
// rtl={isRtlMode && false}
>
{timerOptions.map(item => (
<Item
key={item.value}
onClick={async () => {
await setDisappearingMessagesByConvoId(selectedConvoId, item.value);
}}
>
{item.name}
</Item>
))}
</Submenu>
);
};
/**
* 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
<Submenu
label={window.i18n('notificationForConvo') as any}
// rtl={isRtlMode && false}
>
{(notificationForConvoOptions || []).map(item => {
const disabled = item.value === currentNotificationSetting;
return (
<Item
key={item.value}
onClick={async () => {
await setNotificationForConvoId(selectedConvoId, item.value);
}}
disabled={disabled}
>
{item.name}
</Item>
);
})}
</Submenu>
);
return null;
};

@ -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 (
<SessionContextMenuContainer>
<Menu id={triggerId} animation={animation.fade}>
<AcceptMenuItem />
<DeclineMenuItem />
<NotificationForConvoMenuItem />
<AcceptMsgRequestMenuItem />
<DeclineMsgRequestMenuItem />
<DeclineAndBlockMsgRequestMenuItem />
<PinConversationMenuItem />
<BlockMenuItem />
<CopyMenuItem />

@ -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 (
<Item
onClick={() => {
@ -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 (
<Item
onClick={() => {
@ -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 (
<Item
onClick={() => {
@ -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 (
<Item
onClick={() => {
@ -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 (
<Item
@ -427,8 +357,8 @@ export const CopyMenuItem = (): JSX.Element | null => {
export const MarkAllReadMenuItem = (): JSX.Element | null => {
const convoId = useContext(ContextConversationId);
const isRequest = useIsRequest(convoId);
if (!isRequest) {
const isIncomingRequest = useIsIncomingRequest(convoId);
if (!isIncomingRequest) {
return (
<Item onClick={() => markAllReadByConvoId(convoId)}>{window.i18n('markAllAsRead')}</Item>
);
@ -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
<Submenu
label={window.i18n('disappearingMessages')}
// rtl={isRtlMode && false}
>
{timerOptions.map(item => (
<Item
key={item.value}
onClick={async () => {
await setDisappearingMessagesByConvoId(convoId, item.value);
}}
>
{item.name}
</Item>
))}
</Submenu>
);
}
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
<Submenu
label={window.i18n('notificationForConvo') as any}
// rtl={isRtlMode && false}
>
{(notificationForConvoOptions || []).map(item => {
const disabled = item.value === currentNotificationSetting;
return (
<Item
key={item.value}
onClick={async () => {
await setNotificationForConvoId(convoId, item.value);
}}
disabled={disabled}
>
{item.name}
</Item>
);
})}
</Submenu>
);
}
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 (
<Item onClick={() => clearNickNameByConvoId(convoId)}>{window.i18n('clearNickname')}</Item>
);
if (isMe || !hasNickname || !isPrivate || !isPrivateAndFriend) {
return null;
}
return null;
return (
<Item onClick={() => clearNickNameByConvoId(convoId)}>{window.i18n('clearNickname')}</Item>
);
};
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 (
<Item
onClick={() => {
dispatch(changeNickNameModal({ conversationId: convoId }));
}}
>
{window.i18n('changeNickname')}
</Item>
);
}
return null;
};
export const DeleteMessagesMenuItem = () => {
const convoId = useContext(ContextConversationId);
const isRequest = useIsRequest(convoId);
if (isRequest) {
if (isMe || !isPrivate || !isPrivateAndFriend) {
return null;
}
return (
<Item
onClick={() => {
deleteAllMessagesByConvoIdWithConfirmation(convoId);
dispatch(changeNickNameModal({ conversationId: convoId }));
}}
>
{window.i18n('deleteMessages')}
{window.i18n('changeNickname')}
</Item>
);
};
export const HideBannerMenuItem = (): JSX.Element => {
const dispatch = useDispatch();
export const DeleteMessagesMenuItem = () => {
const convoId = useContext(ContextConversationId);
if (!convoId) {
return null;
}
return (
<Item
onClick={() => {
dispatch(hideMessageRequestBanner());
deleteAllMessagesByConvoIdWithConfirmation(convoId);
}}
>
{window.i18n('hideBanner')}
{window.i18n('deleteMessages')}
</Item>
);
};
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 (
<Item
onClick={async () => {
@ -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 (
<Item
onClick={() => {
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 (
<Item
onClick={() => {
declineConversationWithConfirm({
conversationId: convoId,
syncToDevices: true,
blockContact: true,
currentlySelectedConvo: selected || undefined,
});
}}
>
{window.i18n('block')}
</Item>
);
}
return null;
};

@ -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 (
<Item
onClick={() => {
dispatch(hideMessageRequestBanner());
}}
>
{window.i18n('hideBanner')}
</Item>
);
};
export const MessageRequestBannerContextMenu = (props: PropsContextConversationItem) => {
const { triggerId } = props;
return (
@ -20,11 +35,3 @@ const MessageRequestBannerContextMenu = (props: PropsContextConversationItem) =>
</SessionContextMenuContainer>
);
};
function propsAreEqual(prev: PropsContextConversationItem, next: PropsContextConversationItem) {
return _.isEqual(prev, next);
}
export const MemoMessageRequestBannerContextMenu = React.memo(
MessageRequestBannerContextMenu,
propsAreEqual
);

@ -134,7 +134,7 @@ export const BlockedContactsList = () => {
{hasAtLeastOneSelected && expanded ? (
<SessionButton
buttonColor={SessionButtonColor.Danger}
text={window.i18n('unblockUser')}
text={window.i18n('unblock')}
onClick={unBlockThoseUsers}
dataTestId="unblock-button-settings-screen"
/>

@ -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,
})
);
}

@ -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()) {

@ -167,44 +167,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
);
}
/**
* 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<ConversationAttributes> {
* 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<ConversationAttributes> {
* 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<ConversationMode
}
}
ConversationCollection.prototype.model = ConversationModel;
export function hasValidOutgoingRequestValues({
isMe,
didApproveMe,
isApproved,
isBlocked,
isPrivate,
activeAt,
}: {
isMe: boolean;
isApproved: boolean;
didApproveMe: boolean;
isBlocked: boolean;
isPrivate: boolean;
activeAt: number;
}): boolean {
const isActive = activeAt && isFinite(activeAt) && activeAt > 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);
}

@ -113,6 +113,7 @@ async function insertContactFromDBIntoWrapperAndRefresh(id: string): Promise<voi
*/
async function refreshMappedValue(id: string, duringAppStart = false) {
const fromWrapper = await ContactsWrapperActions.get(id);
if (fromWrapper) {
setMappedValue(fromWrapper);
if (!duringAppStart) {
@ -120,6 +121,14 @@ async function refreshMappedValue(id: string, duringAppStart = false) {
.get(id)
?.triggerUIRefresh();
}
} else {
if (mappedContactWrapperValues.delete(id)) {
if (!duringAppStart) {
getConversationController()
.get(id)
?.triggerUIRefresh();
}
}
}
}
@ -131,10 +140,18 @@ function getContactCached(id: string) {
return mappedContactWrapperValues.get(id);
}
async function removeContactFromWrapper(id: string) {
try {
await ContactsWrapperActions.erase(id);
} catch (e) {
window.log.warn(`ContactsWrapperActions.erase of ${id} failed with ${e.message}`);
}
}
export const SessionUtilContact = {
isContactToStoreInContactsWrapper,
insertAllContactsIntoContactsWrapper,
insertContactFromDBIntoWrapperAndRefresh,
removeContactFromWrapper,
getContactCached,
refreshMappedValue,
};

@ -1,20 +1,20 @@
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { getConversationController } from '../../session/conversations';
import { Data } from '../../data/data';
import {
MessageModelType,
PropsForDataExtractionNotification,
PropsForMessageRequestResponse,
} from '../../models/messageType';
import { omit } from 'lodash';
import { ReplyingToMessageProps } from '../../components/conversation/composition/CompositionBox';
import { QuotedAttachmentType } from '../../components/conversation/message/message-content/Quote';
import { LightBoxOptions } from '../../components/conversation/SessionConversation';
import { Data } from '../../data/data';
import {
CONVERSATION_PRIORITIES,
ConversationNotificationSettingType,
ConversationTypeEnum,
} from '../../models/conversationAttributes';
import {
MessageModelType,
PropsForDataExtractionNotification,
PropsForMessageRequestResponse,
} from '../../models/messageType';
import { getConversationController } from '../../session/conversations';
import { ReactionList } from '../../types/Reaction';
export type CallNotificationType = 'missed-call' | 'started-call' | 'answered-a-call';
@ -571,7 +571,20 @@ const conversationsSlice = createSlice({
},
openRightPanel(state: ConversationsStateType) {
return { ...state, showRightPanel: true };
if (
state.selectedConversation === undefined ||
!state.conversationLookup[state.selectedConversation]
) {
return state;
}
const selected = state.conversationLookup[state.selectedConversation];
// we can open the right panel always for non private chats. and also when the chat is private, and we are friends with the other person
if (!selected.isPrivate || (selected.isApproved && selected.didApproveMe)) {
return { ...state, showRightPanel: true };
}
return state;
},
closeRightPanel(state: ConversationsStateType) {
return { ...state, showRightPanel: false };

@ -25,7 +25,7 @@ import { MessageStatusSelectorProps } from '../../components/conversation/messag
import { MessageTextSelectorProps } from '../../components/conversation/message/message-content/MessageText';
import { GenericReadableMessageSelectorProps } from '../../components/conversation/message/message-item/GenericReadableMessage';
import { LightBoxOptions } from '../../components/conversation/SessionConversation';
import { ConversationModel } from '../../models/conversation';
import { hasValidIncomingRequestValues } from '../../models/conversation';
import {
CONVERSATION_PRIORITIES,
ConversationTypeEnum,
@ -364,15 +364,16 @@ const _getConversationRequests = (
sortedConversations: Array<ReduxConversationType>
): Array<ReduxConversationType> => {
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;
});
};

@ -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);
}

@ -240,8 +240,8 @@ export type LocalizerKeys =
| 'multipleJoinedTheGroup'
| 'kickedFromTheGroup'
| 'multipleKickedFromTheGroup'
| 'blockUser'
| 'unblockUser'
| 'block'
| 'unblock'
| 'unblocked'
| 'blocked'
| 'blockedSettingsTitle'

Loading…
Cancel
Save