Requesting flow working with sending message as acceptance.

This commit is contained in:
warrickct 2022-02-10 16:49:56 +11:00
parent cdeac8f424
commit d627b8e11d
26 changed files with 531 additions and 171 deletions

View File

@ -435,7 +435,7 @@
"notificationSubtitle": "Notifications - $setting$",
"surveyTitle": "Take our Session Survey",
"goToOurSurvey": "Go to our survey",
"blockAll": "Block All",
"clearAll": "Clear All",
"messageRequests": "Message Requests",
"requestsSubtitle": "Pending Requests",
"requestsPlaceholder": "No requests",
@ -467,5 +467,9 @@
"trimDatabaseDescription": "Reduces your message database size to your last 10,000 messages.",
"trimDatabaseConfirmationBody": "Are you sure you want to delete your $deleteAmount$ oldest received messages?",
"reportAsSpam": "REPORT AS SPAM",
"messageRequestPending": "Your message request is currently pending",
"messageRequestAccepted": "Your message request has been accepted",
"messageRequestAcceptedOurs": "You have accepted $name$'s message request",
"declineRequestMessage": "Are you sure you want to decline this message request?",
"respondingToRequestWarning": "Sending a message to this user will automatically accept their message request and reveal your Session ID."
}

View File

@ -840,6 +840,7 @@ const LOKI_SCHEMA_VERSIONS = [
updateToLokiSchemaVersion17,
updateToLokiSchemaVersion18,
updateToLokiSchemaVersion19,
updateToLokiSchemaVersion20,
];
function updateToLokiSchemaVersion1(currentVersion, db) {
@ -1335,6 +1336,55 @@ function updateToLokiSchemaVersion19(currentVersion, db) {
console.log(`updateToLokiSchemaVersion${targetVersion}: success!`);
}
function updateToLokiSchemaVersion20(currentVersion, db) {
const targetVersion = 20;
// if (currentVersion >= targetVersion) {
// return;
// }
console.log(`updateToLokiSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
UPDATE ${CONVERSATIONS_TABLE} SET
json = json_set(json, '$.didApproveMe', 1, '$.isApproved', 1)
WHERE type = 'private';
`);
// all closed group admins
const closedGroupRows = db
.prepare(
`
SELECT json FROM ${CONVERSATIONS_TABLE} WHERE
type = 'group' AND
id NOT LIKE 'publicChat:%';
`
)
.all();
console.warn({ closedGroupRows });
const adminIds = closedGroupRows.map(json => {
return jsonToObject(json).groupAdmins;
});
console.warn({ adminIds });
forEach(adminIds, id => {
db.exec(
`
UPDATE ${CONVERSATIONS_TABLE} SET
json = json_set(json, '$.didApproveMe', 1, '$.isApproved', 1)
WHERE type = id
values ($id);
`
).run({
id,
});
});
writeLokiSchemaVersion(targetVersion, db);
})();
console.log(`updateToLokiSchemaVersion${targetVersion}: success!`);
}
function writeLokiSchemaVersion(newVersion, db) {
db.prepare(
`INSERT INTO loki_schema(

View File

@ -37,7 +37,7 @@ window.isBehindProxy = () => Boolean(config.proxyUrl);
window.lokiFeatureFlags = {
useOnionRequests: true,
useMessageRequests: false,
useMessageRequests: true,
useCallMessage: true,
};

View File

@ -47,11 +47,13 @@ import {
} from '../../types/attachments/VisualAttachment';
import { blobToArrayBuffer } from 'blob-util';
import { MAX_ATTACHMENT_FILESIZE_BYTES } from '../../session/constants';
import { useSelector } from 'react-redux';
import { getOverlayMode } from '../../state/selectors/section';
import styled from 'styled-components';
import { Flex } from '../basic/Flex';
import { blockConvoById } from '../../interactions/conversationInteractions';
import {
acceptConversation,
blockConvoById,
declineConversation,
} from '../../interactions/conversationInteractions';
import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/syncUtils';
// tslint:disable: jsx-curly-spacing
interface State {
@ -226,13 +228,39 @@ export class SessionConversation extends React.Component<Props, State> {
return <MessageView />;
}
// // TODO: refactor component and use overlay hook
// const overlayMode = useSelector(getOverlayMode);
// // either use overlayMode == messageRequest or use Conversation.isAPproved === false;
const conversation = getConversationController().get(selectedConversation.id);
const isApproved = conversation.isApproved();
const selectionMode = selectedMessages.length > 0;
const useMsgRequests =
window.lokiFeatureFlags.useMessageRequests &&
window.inboxStore?.getState().userConfig.messageRequests;
const showMsgRequestUI = useMsgRequests && !isApproved && messagesProps.length > 0;
const handleDeclineConversationRequest = async () => {
window.inboxStore?.dispatch(
updateConfirmModal({
okText: window.i18n('decline'),
cancelText: window.i18n('cancel'),
message: window.i18n('declineRequestMessage'),
onClickOk: async () => {
declineConversation(selectedConversation.id, false);
blockConvoById(selectedConversation.id);
forceSyncConfigurationNowIfNeeded();
},
onClickCancel: () => {
window.inboxStore?.dispatch(updateConfirmModal(null));
},
onClickClose: () => {
window.inboxStore?.dispatch(updateConfirmModal(null));
},
})
);
};
const handleAcceptConversationRequest = async () => {
const { id } = selectedConversation;
await acceptConversation(id, true);
};
return (
<SessionTheme>
@ -252,34 +280,29 @@ export class SessionConversation extends React.Component<Props, State> {
{lightBoxOptions?.media && this.renderLightBox(lightBoxOptions)}
<div className="conversation-messages">
{!isApproved && (
{showMsgRequestUI && (
<ConversationRequestBanner>
<div className="conversation-request-banner__row">
<SessionButton
buttonColor={SessionButtonColor.Green}
buttonType={SessionButtonType.BrandOutline}
onClick={() => {
getConversationController()
.get(selectedConversation.id)
.setIsApproved(true);
}}
onClick={handleAcceptConversationRequest}
text={window.i18n('accept')}
/>
<SessionButton
buttonColor={SessionButtonColor.Danger}
buttonType={SessionButtonType.BrandOutline}
text={window.i18n('decline')}
onClick={async () => {
getConversationController();
blockConvoById(selectedConversation.id);
}}
onClick={handleDeclineConversationRequest}
/>
</div>
{/*
Disabling for now until report as spam is added in
<SessionButton
buttonColor={SessionButtonColor.Danger}
buttonType={SessionButtonType.Simple}
text={window.i18n('reportAsSpam')}
/>
/> */}
</ConversationRequestBanner>
)}
<SplitViewContainer
@ -293,7 +316,7 @@ export class SessionConversation extends React.Component<Props, State> {
{isDraggingFile && <SessionFileDropzone />}
</div>
{!isApproved && (
{showMsgRequestUI && (
<ConversationRequestTextBottom>
<ConversationRequestTextInner>
{window.i18n('respondingToRequestWarning')}

View File

@ -2,7 +2,11 @@ import React from 'react';
import { useSelector } from 'react-redux';
// tslint:disable-next-line: no-submodule-imports
import useKey from 'react-use/lib/useKey';
import { PropsForDataExtractionNotification, QuoteClickOptions } from '../../models/messageType';
import {
PropsForDataExtractionNotification,
PropsForMessageRequestResponse,
QuoteClickOptions,
} from '../../models/messageType';
import {
PropsForCallNotification,
PropsForExpirationTimer,
@ -11,7 +15,7 @@ import {
} from '../../state/ducks/conversations';
import { getSortedMessagesTypesOfSelectedConversation } from '../../state/selectors/conversations';
import { GroupUpdateMessage } from './message/message-item/GroupUpdateMessage';
import { DataExtractionNotification } from './message/message-item/DataExtractionNotification';
import { MessageRequestResponse } from './message/message-item/MessageRequestResponse';
import { MessageDateBreak } from './message/message-item/DateBreak';
import { GroupInvitation } from './message/message-item/GroupInvitation';
import { Message } from './message/message-item/Message';
@ -19,6 +23,7 @@ import { CallNotification } from './message/message-item/notification-bubble/Cal
import { SessionLastSeenIndicator } from './SessionLastSeenIndicator';
import { TimerNotification } from './TimerNotification';
import { DataExtractionNotification } from './message/message-item/DataExtractionNotification';
function isNotTextboxEvent(e: KeyboardEvent) {
return (e?.target as any)?.type === undefined;
@ -79,6 +84,16 @@ export const SessionMessagesList = (props: {
return [<GroupInvitation key={messageId} {...msgProps} />, dateBreak, unreadIndicator];
}
if (messageProps.message?.messageType === 'message-request-response') {
const msgProps = messageProps.message.props as PropsForMessageRequestResponse;
return [
<MessageRequestResponse key={messageId} {...msgProps} />,
dateBreak,
unreadIndicator,
];
}
if (messageProps.message?.messageType === 'data-extraction') {
const msgProps = messageProps.message.props as PropsForDataExtractionNotification;

View File

@ -424,16 +424,27 @@ class CompositionBoxInner extends React.Component<Props, State> {
return null;
}
const { isKickedFromGroup, left, isPrivate, isBlocked } = this.props.selectedConversation;
const messagePlaceHolder = isKickedFromGroup
? i18n('youGotKickedFromGroup')
: left
? i18n('youLeftTheGroup')
: isBlocked && isPrivate
? i18n('unblockToSend')
: isBlocked && !isPrivate
? i18n('unblockGroupToSend')
: i18n('sendMessage');
const {
isKickedFromGroup,
left,
isPrivate,
isBlocked,
didApproveMe,
isApproved,
} = this.props.selectedConversation;
const messagePlaceHolder =
// isApproved && !didApproveMe && isPrivate
isApproved && !didApproveMe && isPrivate
? i18n('messageRequestPending')
: isKickedFromGroup
? i18n('youGotKickedFromGroup')
: left
? i18n('youLeftTheGroup')
: isBlocked && isPrivate
? i18n('unblockToSend')
: isBlocked && !isPrivate
? i18n('unblockGroupToSend')
: i18n('sendMessage');
const { typingEnabled } = this.props;
return (
@ -785,6 +796,14 @@ class CompositionBoxInner extends React.Component<Props, State> {
ToastUtils.pushUnblockToSend();
return;
}
if (
selectedConversation.isApproved &&
!selectedConversation.didApproveMe &&
selectedConversation.isPrivate
) {
ToastUtils.pushMessageRequestPending();
return;
}
if (selectedConversation.isBlocked && !selectedConversation.isPrivate) {
ToastUtils.pushUnblockToSendGroup();
return;

View File

@ -0,0 +1,44 @@
import React from 'react';
import { PropsForMessageRequestResponse } from '../../../../models/messageType';
import { getConversationController } from '../../../../session/conversations';
import { UserUtils } from '../../../../session/utils';
import { Flex } from '../../../basic/Flex';
import { SpacerSM, Text } from '../../../basic/Text';
import { ReadableMessage } from './ReadableMessage';
export const MessageRequestResponse = (props: PropsForMessageRequestResponse) => {
const { messageId, isUnread, receivedAt, conversationId, source } = props;
let profileName = '';
if (conversationId) {
profileName =
getConversationController()
.get(conversationId)
.getProfileName() + '';
}
const msgText =
profileName && props.source === UserUtils.getOurPubKeyStrFromCache()
? window.i18n('messageRequestAcceptedOurs', [profileName])
: window.i18n('messageRequestAccepted');
return (
<ReadableMessage
messageId={messageId}
receivedAt={receivedAt}
isUnread={isUnread}
key={`readable-message-${messageId}`}
>
<Flex
container={true}
flexDirection="row"
alignItems="center"
justifyContent="center"
margin={'var(--margins-sm)'}
id={`msg-${messageId}`}
>
<SpacerSM />
<Text text={msgText} subtle={true} ellipsisOverflow={true} />
</Flex>
</ReadableMessage>
);
};

View File

@ -28,6 +28,8 @@ export const LeftPaneSectionHeader = (props: { buttonClicked?: any }) => {
const isMessageSection = focusedSection === SectionType.Message;
const isMessageRequestOverlay = overlayMode === 'message-requests';
const showBackButton = isMessageRequestOverlay && isMessageSection;
switch (focusedSection) {
case SectionType.Contact:
label = window.i18n('contactsHeader');
@ -46,7 +48,7 @@ export const LeftPaneSectionHeader = (props: { buttonClicked?: any }) => {
return (
<Flex flexDirection="column">
<div className="module-left-pane__header">
{isMessageRequestOverlay && (
{showBackButton && (
<SessionIconButton
onClick={() => {
dispatch(setOverlayMode(undefined));

View File

@ -6,7 +6,6 @@ import { MessageBody } from '../../conversation/message/message-content/MessageB
import { OutgoingMessageStatus } from '../../conversation/message/message-content/OutgoingMessageStatus';
import { TypingAnimation } from '../../conversation/TypingAnimation';
import { ContextConversationId } from './ConversationListItem';
import { MessageRequestButtons } from './MessageRequest';
function useMessageItemProps(convoId: string) {
const convoProps = useConversationPropsById(convoId);
@ -51,7 +50,6 @@ export const MessageItem = (props: { isMessageRequest: boolean }) => {
<MessageBody isGroup={true} text={text} disableJumbomoji={true} disableLinks={true} />
)}
</div>
<MessageRequestButtons isMessageRequest={props.isMessageRequest} />
{lastMessage && lastMessage.status && !props.isMessageRequest ? (
<OutgoingMessageStatus status={lastMessage.status} />
) : null}

View File

@ -1,64 +0,0 @@
import React, { useContext } from 'react';
import {
approveConversation,
blockConvoById,
} from '../../../interactions/conversationInteractions';
import { forceSyncConfigurationNowIfNeeded } from '../../../session/utils/syncUtils';
import { SessionIconButton } from '../../icon';
import { ContextConversationId } from './ConversationListItem';
const RejectMessageRequestButton = () => {
const conversationId = useContext(ContextConversationId);
/**
* Removes conversation from requests list,
* adds ID to block list, syncs the block with linked devices.
*/
const handleConversationBlock = async () => {
await blockConvoById(conversationId);
await forceSyncConfigurationNowIfNeeded();
};
return (
<SessionIconButton
iconType="exit"
iconSize="large"
onClick={handleConversationBlock}
backgroundColor="var(--color-destructive)"
iconColor="var(--color-foreground-primary)"
iconPadding="var(--margins-xs)"
borderRadius="2px"
margin="0 5px 0 0"
/>
);
};
const ApproveMessageRequestButton = () => {
const conversationId = useContext(ContextConversationId);
return (
<SessionIconButton
iconType="check"
iconSize="large"
onClick={async () => {
await approveConversation(conversationId);
}}
backgroundColor="var(--color-accent)"
iconColor="var(--color-foreground-primary)"
iconPadding="var(--margins-xs)"
borderRadius="2px"
/>
);
};
export const MessageRequestButtons = ({ isMessageRequest }: { isMessageRequest: boolean }) => {
if (!isMessageRequest) {
return null;
}
return (
<>
<RejectMessageRequestButton />
<ApproveMessageRequestButton />
</>
);
};

View File

@ -35,20 +35,21 @@ async function handleBlockAllRequestsClick(messageRequestSetting: boolean) {
return;
}
const conversationRequests = conversations.filter(
const convoRequestsToBlock = conversations.filter(
c => c.isPrivate() && c.get('active_at') && c.get('isApproved')
);
let syncRequired = false;
if (!conversationRequests) {
if (!convoRequestsToBlock) {
window?.log?.info('No conversation requests to block.');
return;
}
await Promise.all(
conversationRequests.map(async convo => {
convoRequestsToBlock.map(async convo => {
await BlockedNumberController.block(convo.id);
await convo.setIsApproved(false);
syncRequired = true;
})
);
@ -67,7 +68,7 @@ export const OverlayMessageRequest = () => {
const messageRequestSetting = useSelector(getIsMessageRequestsEnabled);
const buttonText = window.i18n('blockAll');
const buttonText = window.i18n('clearAll');
return (
<div className="module-left-pane-overlay">

View File

@ -121,9 +121,9 @@ export async function unblockConvoById(conversationId: string) {
}
/**
* marks the conversation as approved.
* marks the conversation's approval fields, sends messageRequestResponse, syncs to linked devices
*/
export const approveConversation = async (conversationId: string) => {
export const acceptConversation = async (conversationId: string, syncToDevices: boolean = true) => {
const conversationToApprove = getConversationController().get(conversationId);
if (!conversationToApprove || conversationToApprove.isApproved()) {
@ -131,10 +131,38 @@ export const approveConversation = async (conversationId: string) => {
return;
}
await conversationToApprove.setIsApproved(true);
Promise.all([
await conversationToApprove.setIsApproved(true),
await conversationToApprove.setDidApproveMe(true),
]);
await conversationToApprove.sendMessageRequestResponse(true);
// Conversation was not approved before so a sync is needed
await forceSyncConfigurationNowIfNeeded();
if (syncToDevices) {
await forceSyncConfigurationNowIfNeeded();
}
};
/**
* Sets the approval fields to false for conversation. Sends decline message.
*/
export const declineConversation = 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) {

View File

@ -4,12 +4,12 @@ import { getMessageQueue } from '../session';
import { getConversationController } from '../session/conversations';
import { ClosedGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage';
import { PubKey } from '../session/types';
import { UserUtils } from '../session/utils';
import { ToastUtils, UserUtils } from '../session/utils';
import { BlockedNumberController } from '../util';
import { leaveClosedGroup } from '../session/group/closed-group';
import { SignalService } from '../protobuf';
import { MessageModel } from './message';
import { MessageAttributesOptionals, MessageModelType } from './messageType';
import { MessageAttributesOptionals, MessageDirection, MessageModelType } from './messageType';
import autoBind from 'auto-bind';
import {
getMessagesByConversation,
@ -19,7 +19,7 @@ import {
saveMessages,
updateConversation,
} from '../../ts/data/data';
import { toHex } from '../session/utils/String';
import { fromHexToArray, toHex } from '../session/utils/String';
import {
actions as conversationActions,
conversationChanged,
@ -59,6 +59,8 @@ import {
getAbsoluteAttachmentPath,
loadAttachmentData,
} from '../types/MessageAttachment';
import { getOurPubKeyStrFromCache } from '../session/utils/User';
import { MessageRequestResponse } from '../session/messages/outgoing/controlMessage/MessageRequestResponse';
export enum ConversationTypeEnum {
GROUP = 'group',
@ -112,6 +114,7 @@ export interface ConversationAttributes {
isTrustedForAttachmentDownload: boolean;
isPinned: boolean;
isApproved: boolean;
didApproveMe: boolean;
}
export interface ConversationAttributesOptionals {
@ -151,6 +154,7 @@ export interface ConversationAttributesOptionals {
isTrustedForAttachmentDownload?: boolean;
isPinned: boolean;
isApproved?: boolean;
didApproveMe?: boolean;
}
/**
@ -180,6 +184,7 @@ export const fillConvoAttributesWithDefaults = (
isTrustedForAttachmentDownload: false, // we don't trust a contact until we say so
isPinned: false,
isApproved: false,
didApproveMe: false,
});
};
@ -341,6 +346,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const subscriberCount = this.get('subscriberCount');
const isPinned = this.isPinned();
const isApproved = this.isApproved();
const didApproveMe = this.didApproveMe();
const hasNickname = !!this.getNickname();
const isKickedFromGroup = !!this.get('isKickedFromGroup');
const left = !!this.get('left');
@ -416,6 +422,9 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
if (isPinned) {
toRet.isPinned = isPinned;
}
if (isApproved) {
toRet.didApproveMe = didApproveMe;
}
if (isApproved) {
toRet.isApproved = isApproved;
}
@ -634,11 +643,28 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
lokiProfile: UserUtils.getOurProfile(),
};
const updateApprovalNeeded =
!this.isApproved() && (this.isPrivate() || this.isMediumGroup() || this.isClosedGroup());
if (updateApprovalNeeded) {
const shouldApprove = !this.isApproved() && this.isPrivate();
const hasMsgsFromOther =
(await getMessagesByConversation(this.id, { type: MessageDirection.incoming })).length > 0;
console.warn(hasMsgsFromOther);
if (shouldApprove) {
await this.setIsApproved(true);
void forceSyncConfigurationNowIfNeeded();
if (!this.didApproveMe() && hasMsgsFromOther) {
console.warn('This is a reply message sending message request acceptance response.');
// TODO: if this is a reply, send messageRequestAccept
await this.setDidApproveMe(true);
await this.sendMessageRequestResponse(true);
void forceSyncConfigurationNowIfNeeded();
}
// void forceSyncConfigurationNowIfNeeded();
}
// TODO: remove once dev-tested
if (chatMessageParams.body?.includes('unapprove')) {
await this.setIsApproved(false);
await this.setDidApproveMe(false);
// void forceSyncConfigurationNowIfNeeded();
}
if (this.isOpenGroupV2()) {
@ -705,6 +731,37 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
}
public async sendMessageRequestResponse(isApproved: boolean) {
const publicKey = fromHexToArray(getOurPubKeyStrFromCache());
// const msgRequestResponseParams:
const timestamp = Date.now();
const messageRequestResponseParams = {
timestamp,
publicKey,
isApproved,
};
const messageRequestResponse = new MessageRequestResponse(messageRequestResponseParams);
if (this.isPrivate()) {
// 1-1 conversations
const pubkeyForSending = new PubKey(this.id);
await getMessageQueue()
.sendToPubKey(pubkeyForSending, messageRequestResponse)
.catch(window?.log?.error);
}
// TODO: may be removable as group invites are implied to be friends.
// else if (this.isClosedGroup()) {
// // group conversations
// await getMessageQueue()
// .sendToGroup(messageRequestResponse, undefined, new PubKey(this.id))
// .catch(window?.log?.error);
// }
console.warn('Sent message request response', isApproved);
}
public async sendMessage(msg: SendMessageType) {
const { attachments, body, groupInvitation, preview, quote } = msg;
this.clearTypingTimers();
@ -923,16 +980,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
public async addSingleMessage(messageAttributes: MessageAttributesOptionals, setToExpire = true) {
const model = new MessageModel(messageAttributes);
const isMe = messageAttributes.source === UserUtils.getOurPubKeyStrFromCache();
if (
isMe &&
window.lokiFeatureFlags.useMessageRequests &&
window.inboxStore?.getState().userConfig.messageRequests
) {
await this.setIsApproved(true);
}
// no need to trigger a UI update now, we trigger a messageAdded just below
const messageId = await model.commit(false);
model.set({ id: messageId });
@ -1180,6 +1227,23 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
isApproved: value,
});
if (!this.isApproved() && value) {
// if it's false or hasnt been set, send approval msg
this.sendMessageRequestResponse(true);
void forceSyncConfigurationNowIfNeeded();
}
await this.commit();
}
}
public async setDidApproveMe(value: boolean) {
if (value !== this.didApproveMe()) {
window?.log?.info(`Setting ${this.attributes.profileName} didApproveMe to:: ${value}`);
this.set({
didApproveMe: value,
});
await this.commit();
}
}
@ -1271,6 +1335,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return Boolean(this.get('isPinned'));
}
public didApproveMe() {
return Boolean(this.get('didApproveMe'));
}
public isApproved() {
return Boolean(this.get('isApproved'));
}
@ -1379,6 +1447,11 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
const conversationId = this.id;
if (!this.isApproved()) {
window?.log?.info('notification cancelled for unapproved convo', this.idForLogging());
return;
}
// make sure the notifications are not muted for this convo (and not the source convo)
const convNotif = this.get('triggerNotificationsFor');
if (convNotif === 'disabled') {

View File

@ -16,6 +16,7 @@ import {
MessageGroupUpdate,
MessageModelType,
PropsForDataExtractionNotification,
PropsForMessageRequestResponse,
} from './messageType';
import autoBind from 'auto-bind';
@ -106,6 +107,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
const propsForGroupInvitation = this.getPropsForGroupInvitation();
const propsForGroupUpdateMessage = this.getPropsForGroupUpdateMessage();
const propsForTimerNotification = this.getPropsForTimerNotification();
const propsForMessageRequestResponse = this.getPropsForMessageRequestResponse();
const callNotificationType = this.get('callNotificationType');
const messageProps: MessageModelPropsWithoutConvoProps = {
propsForMessage: this.getPropsForMessage(),
@ -113,6 +115,9 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
if (propsForDataExtractionNotification) {
messageProps.propsForDataExtractionNotification = propsForDataExtractionNotification;
}
if (propsForMessageRequestResponse) {
messageProps.propsForMessageRequestResponse = propsForMessageRequestResponse;
}
if (propsForGroupInvitation) {
messageProps.propsForGroupInvitation = propsForGroupInvitation;
}
@ -176,6 +181,10 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
return !!this.get('groupInvitation');
}
public isMessageRequestResponse() {
return !!this.get('messageRequestResponse');
}
public isDataExtractionNotification() {
return !!this.get('dataExtractionNotification');
}
@ -300,6 +309,30 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
};
}
public getPropsForMessageRequestResponse(): PropsForMessageRequestResponse | null {
if (!this.isMessageRequestResponse()) {
return null;
}
const messageRequestResponse = this.get('messageRequestResponse');
if (!messageRequestResponse) {
window.log.warn('messageRequestResponse should not happen');
return null;
}
const contact = this.findAndFormatContact(messageRequestResponse.source);
return {
...messageRequestResponse,
name: contact.profileName || contact.name || messageRequestResponse.source,
messageId: this.id,
receivedAt: this.get('received_at'),
isUnread: this.isUnread(),
conversationId: this.get('conversationId'),
source: this.get('source'),
};
}
public findContact(pubkey: string) {
return getConversationController().get(pubkey);
}

View File

@ -98,6 +98,11 @@ export interface MessageAttributes {
*/
dataExtractionNotification?: DataExtractionNotificationMsg;
/**
* For displaying a message to notifying when a request has been accepted.
*/
messageRequestResponse?: MessageRequestResponseMsg;
/**
* This field is used for unsending messages and used in sending unsend message requests.
*/
@ -117,6 +122,11 @@ export interface DataExtractionNotificationMsg {
referencedAttachmentTimestamp: number; // the attachment timestamp he screenshot
}
export interface MessageRequestResponseMsg {
source: string;
isApproved: boolean;
}
export enum MessageDirection {
outgoing = 'outgoing',
incoming = 'incoming',
@ -129,6 +139,17 @@ export type PropsForDataExtractionNotification = DataExtractionNotificationMsg &
isUnread: boolean;
};
export type PropsForMessageRequestResponse = MessageRequestResponseMsg & {
conversationId?: string;
name?: string;
messageId: string;
receivedAt?: number;
isUnread: boolean;
isApproved?: boolean;
publicKey?: string;
source?: string;
};
export type MessageGroupUpdate = {
left?: Array<string>;
joined?: Array<string>;
@ -172,6 +193,11 @@ export interface MessageAttributesOptionals {
source: string;
referencedAttachmentTimestamp: number;
};
messageRequestResponse?: {
/** 1 means approved, 0 means unapproved. */
isApproved?: number;
publicKey?: string;
};
unread?: number;
group?: any;
timestamp?: number;

View File

@ -111,6 +111,18 @@ export async function handleClosedGroupControlMessage(
return;
}
if (type === Type.NEW) {
if (
getConversationController()
.get(envelope.senderIdentity)
.isApproved() == false
) {
window?.log?.info(
'Received new closed group message from an unapproved sender -- dropping message.'
);
// TODO: remove console output
console.warn('Received unapproved closed group invite msg');
return;
}
await handleNewClosedGroup(envelope, groupUpdate);
return;
}

View File

@ -146,6 +146,26 @@ const handleContactReceived = async (
) {
if (contactReceived.isApproved) {
await contactConvo.setIsApproved(Boolean(contactReceived.isApproved));
if (contactReceived.didApproveMe) {
await contactConvo.setDidApproveMe(Boolean(contactReceived.didApproveMe));
// if source of the sync matches conversationId
contactConvo.addSingleMessage({
conversationId: contactConvo.get('id'),
source: envelope.source,
type: 'outgoing', // mark it as outgoing just so it appears below our sent attachment
sent_at: _.toNumber(envelope.timestamp), // TODO: maybe add timestamp to messageRequestResponse? confirm it doesn't exist first
received_at: Date.now(),
messageRequestResponse: {
isApproved: 1,
publicKey: UserUtils.getOurPubKeyStrFromCache(), // it's a sync therefore the pubkey would be ours
},
unread: 1, // 1 means unread
expireTimer: 0,
});
contactConvo.updateLastMessage();
}
}
if (contactReceived.isBlocked) {

View File

@ -3,11 +3,11 @@ import { handleDataMessage } from './dataMessage';
import { removeFromCache, updateCache } from './cache';
import { SignalService } from '../protobuf';
import * as Lodash from 'lodash';
import _, * as Lodash from 'lodash';
import { PubKey } from '../session/types';
import { BlockedNumberController } from '../util/blockedNumberController';
import { GroupUtils, UserUtils } from '../session/utils';
import { GroupUtils, ToastUtils, UserUtils } from '../session/utils';
import { fromHexToArray, toHex } from '../session/utils/String';
import { concatUInt8Array, getSodium } from '../session/crypto';
import { getConversationController } from '../session/conversations';
@ -17,12 +17,7 @@ import { ConversationTypeEnum } from '../models/conversation';
import { removeMessagePadding } from '../session/crypto/BufferPadding';
import { perfEnd, perfStart } from '../session/utils/Performance';
import { getAllCachedECKeyPair } from './closedGroups';
import { getMessageBySenderAndTimestamp } from '../data/data';
import { handleCallMessage } from './callMessage';
import {
deleteMessagesFromSwarmAndCompletelyLocally,
deleteMessagesFromSwarmAndMarkAsDeletedLocally,
} from '../interactions/conversations/unsendingInteractions';
import { SettingsKey } from '../data/settings-key';
export async function handleContentMessage(envelope: EnvelopePlus, messageHash: string) {
@ -406,6 +401,13 @@ export async function innerHandleContentMessage(
if (content.callMessage && window.lokiFeatureFlags?.useCallMessage) {
await handleCallMessage(envelope, content.callMessage as SignalService.CallMessage);
}
if (content.messageRequestResponse) {
console.warn('received message request response');
await handleMessageRequestResponse(
envelope,
content.messageRequestResponse as SignalService.MessageRequestResponse
);
}
} catch (e) {
window?.log?.warn(e);
}
@ -509,7 +511,6 @@ async function handleUnsendMessage(envelope: EnvelopePlus, unsendMessage: Signal
return;
}
if (!unsendMessage) {
//#region early exit conditions
window?.log?.error('handleUnsendMessage: Invalid parameters -- dropping message.');
await removeFromCache(envelope);
@ -521,40 +522,63 @@ async function handleUnsendMessage(envelope: EnvelopePlus, unsendMessage: Signal
return;
}
}
const messageToDelete = await getMessageBySenderAndTimestamp({
source: messageAuthor,
timestamp: Lodash.toNumber(timestamp),
});
const messageHash = messageToDelete?.get('messageHash');
//#endregion
/**
* Sets approval fields for conversation depending on response's values. If request is approving, pushes notification and
*/
async function handleMessageRequestResponse(
envelope: EnvelopePlus,
messageRequestResponse: SignalService.MessageRequestResponse
) {
const { isApproved, publicKey } = messageRequestResponse;
//#region executing deletion
if (messageHash && messageToDelete) {
window.log.info('handleUnsendMessage: got a request to delete ', messageHash);
const conversation = getConversationController().get(messageToDelete.get('conversationId'));
if (!conversation) {
await removeFromCache(envelope);
return;
}
if (messageToDelete.getSource() === UserUtils.getOurPubKeyStrFromCache()) {
// a message we sent is completely removed when we get a unsend request
void deleteMessagesFromSwarmAndCompletelyLocally(conversation, [messageToDelete]);
} else {
void deleteMessagesFromSwarmAndMarkAsDeletedLocally(conversation, [messageToDelete]);
}
} else {
window.log.info(
'handleUnsendMessage: got a request to delete an unknown messageHash:',
messageHash,
' and found messageToDelete:',
messageToDelete?.id
);
if (!messageRequestResponse) {
window?.log?.error('handleMessageRequestResponse: Invalid parameters -- dropping message.');
await removeFromCache(envelope);
return;
}
await removeFromCache(envelope);
//#endregion
const convoId = toHex(publicKey);
// TODO: commenting out, including in one larger function for now
// await updateConversationDidApproveMe(toHex(publicKey), isApproved);
const conversationToApprove = getConversationController().get(convoId);
if (!conversationToApprove || conversationToApprove.didApproveMe() === isApproved) {
window?.log?.info(
'Conversation already contains the correct value for the didApproveMe field.'
);
return;
}
// TODO: Maybe move this to conversation interactions
await conversationToApprove.setIsApproved(isApproved);
await conversationToApprove.setDidApproveMe(isApproved);
if (isApproved === true) {
ToastUtils.pushMessageRequestAccepted();
// Conversation was not approved before so a sync is needed
conversationToApprove.addSingleMessage({
conversationId: conversationToApprove.get('id'),
source: envelope.source,
type: 'outgoing', // mark it as outgoing just so it appears below our sent attachment
sent_at: _.toNumber(envelope.timestamp), // TODO: maybe add timestamp to messageRequestResponse? confirm it doesn't exist first
received_at: Date.now(),
messageRequestResponse: {
isApproved: 1,
publicKey: convoId,
},
unread: 1, // 1 means unread
expireTimer: 0,
});
conversationToApprove.updateLastMessage();
}
// await forceSyncConfigurationNowIfNeeded();
await removeFromCache(envelope);
}
/**

View File

@ -223,10 +223,10 @@ async function handleRegularMessage(
if (type === 'outgoing') {
await handleSyncedReceipts(message, conversation);
if (window.lokiFeatureFlags.useMessageRequests) {
// assumes sync receipts are always from linked device outgoings
await conversation.setIsApproved(true);
}
// if (window.lokiFeatureFlags.useMessageRequests) {
// // assumes sync receipts are always from linked device outgoings
// await conversation.setIsApproved(true);
// }
}
const conversationActiveAt = conversation.get('active_at');

View File

@ -95,6 +95,7 @@ export class ConfigurationMessageContact {
public profileKey?: Uint8Array;
public isApproved?: boolean;
public isBlocked?: boolean;
public didApproveMe?: boolean;
public constructor({
publicKey,
@ -103,6 +104,7 @@ export class ConfigurationMessageContact {
profileKey,
isApproved,
isBlocked,
didApproveMe
}: {
publicKey: string;
displayName: string;
@ -110,6 +112,7 @@ export class ConfigurationMessageContact {
profileKey?: Uint8Array;
isApproved?: boolean;
isBlocked?: boolean;
didApproveMe?: boolean;
}) {
this.publicKey = publicKey;
this.displayName = displayName;
@ -117,6 +120,7 @@ export class ConfigurationMessageContact {
this.profileKey = profileKey;
this.isApproved = isApproved;
this.isBlocked = isBlocked;
this.didApproveMe = didApproveMe;
// will throw if public key is invalid
PubKey.cast(publicKey);
@ -141,6 +145,7 @@ export class ConfigurationMessageContact {
profileKey: this.profileKey,
isApproved: this.isApproved,
isBlocked: this.isBlocked,
didApproveMe: this.didApproveMe
});
}
}

View File

@ -22,6 +22,7 @@ import { OpenGroupRequestCommonType } from '../apis/open_group_api/opengroupV2/A
import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage';
import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage';
import { MessageRequestResponse } from '../messages/outgoing/controlMessage/MessageRequestResponse';
type ClosedGroupMessageType =
| ClosedGroupVisibleMessage
@ -32,7 +33,8 @@ type ClosedGroupMessageType =
| ExpirationTimerUpdateMessage
| ClosedGroupEncryptionPairMessage
| UnsendMessage
| ClosedGroupEncryptionPairRequestMessage;
| ClosedGroupEncryptionPairRequestMessage
| MessageRequestResponse;
// ClosedGroupEncryptionPairReplyMessage must be sent to a user pubkey. Not a group.

View File

@ -212,6 +212,10 @@ export function pushTooManyMembers() {
pushToastError('tooManyMembers', window.i18n('closedGroupMaxSize'));
}
export function pushMessageRequestPending() {
pushToastInfo('messageRequestPending', window.i18n('messageRequestPending'));
}
export function pushUnblockToSend() {
pushToastInfo('unblockToSend', window.i18n('unblockToSend'));
}
@ -232,6 +236,11 @@ export function pushDeleted() {
pushToastSuccess('deleted', window.i18n('deleted'), undefined, 'check');
}
export function pushMessageRequestAccepted() {
// TODO: translation
pushToastSuccess('requestAccepted', 'message request accepted', undefined, undefined);
}
export function pushCannotRemoveCreatorFromGroup() {
pushToastWarning(
'cannotRemoveCreatorFromGroup',

View File

@ -28,6 +28,7 @@ import { getV2OpenGroupRoom } from '../../data/opengroups';
import { getCompleteUrlFromRoom } from '../apis/open_group_api/utils/OpenGroupUtils';
import { DURATION } from '../constants';
import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage';
import { MessageRequestResponse } from '../messages/outgoing/controlMessage/MessageRequestResponse';
const ITEM_ID_LAST_SYNC_TIMESTAMP = 'lastSyncedTimestamp';
@ -197,6 +198,7 @@ const getValidContacts = (convos: Array<ConversationModel>) => {
profileKey: !profileKeyForContact?.length ? undefined : profileKeyForContact,
isApproved: c.isApproved(),
isBlocked: c.isBlocked(),
didApproveMe: c.didApproveMe(),
});
} catch (e) {
window?.log.warn('getValidContacts', e);
@ -307,6 +309,7 @@ export type SyncMessageType =
| VisibleMessage
| ExpirationTimerUpdateMessage
| ConfigurationMessage
| MessageRequestResponse
| UnsendMessage;
export const buildSyncMessage = (

View File

@ -10,6 +10,7 @@ import {
MessageDeliveryStatus,
MessageModelType,
PropsForDataExtractionNotification,
PropsForMessageRequestResponse,
} from '../../models/messageType';
import { perfEnd, perfStart } from '../../session/utils/Performance';
import { omit } from 'lodash';
@ -32,6 +33,7 @@ export type MessageModelPropsWithoutConvoProps = {
propsForDataExtractionNotification?: PropsForDataExtractionNotification;
propsForGroupUpdateMessage?: PropsForGroupUpdate;
propsForCallNotification?: PropsForCallNotification;
propsForMessageRequestResponse?: PropsForMessageRequestResponse;
};
export type MessageModelPropsWithConvoProps = SortedMessageModelProps & {
@ -252,6 +254,7 @@ export interface ReduxConversationType {
isPinned?: boolean;
isApproved?: boolean;
didApproveMe?: boolean;
}
export interface NotificationForConvoOption {

View File

@ -171,6 +171,7 @@ export type MessagePropsType =
| 'group-notification'
| 'group-invitation'
| 'data-extraction'
| 'message-request-response'
| 'timer-notification'
| 'regular-message'
| 'unread-indicator'
@ -208,6 +209,17 @@ export const getSortedMessagesTypesOfSelectedConversation = createSelector(
};
}
if (msg.propsForMessageRequestResponse) {
return {
showUnreadIndicator: isFirstUnread,
showDateBreak,
message: {
messageType: 'message-request-response',
props: { ...msg.propsForMessageRequestResponse, messageId: msg.propsForMessage.id },
},
};
}
if (msg.propsForGroupInvitation) {
return {
showUnreadIndicator: isFirstUnread,
@ -412,6 +424,12 @@ export const getSortedConversations = createSelector(
_getSortedConversations
);
/**
*
* @param sortedConversations List of conversations that are valid for both requests and regular conversation inbox
* @param isMessageRequestEnabled Apply message request filtering.
* @returns A list of message request conversations.
*/
const _getConversationRequests = (
sortedConversations: Array<ReduxConversationType>,
isMessageRequestEnabled?: boolean
@ -419,7 +437,14 @@ const _getConversationRequests = (
const pushToMessageRequests =
isMessageRequestEnabled && window?.lokiFeatureFlags?.useMessageRequests;
return _.filter(sortedConversations, conversation => {
return pushToMessageRequests && !conversation.isApproved && !conversation.isBlocked;
console.warn({ conversation });
return (
pushToMessageRequests &&
!conversation.isApproved &&
!conversation.isBlocked &&
conversation.isPrivate &&
!conversation.isMe
);
});
};
@ -442,6 +467,7 @@ const _getPrivateContactsPubkeys = (
conversation.isPrivate &&
!conversation.isBlocked &&
!conversation.isMe &&
(conversation.didApproveMe || !pushToMessageRequests) &&
(conversation.isApproved || !pushToMessageRequests) &&
Boolean(conversation.activeAt)
);

View File

@ -254,7 +254,7 @@ export type LocalizerKeys =
| 'editMenuDeleteContact'
| 'hideMenuBarTitle'
| 'imageCaptionIconAlt'
| 'blockAll'
| 'clearAll'
| 'sendRecoveryPhraseTitle'
| 'multipleJoinedTheGroup'
| 'databaseError'
@ -467,4 +467,8 @@ export type LocalizerKeys =
| 'trimDatabaseConfirmationBody'
| 'reportAsSpam'
| 'respondingToRequestWarning'
| 'messageRequestPending'
| 'messageRequestAccepted'
| 'messageRequestAcceptedOurs'
| 'declineRequestMessage'
| 'reportIssue';