feat: save conversation interaction errors to a message history

pull/2789/head
William Grant 2 years ago
parent 68a94117a0
commit 027b412fb2

@ -11,6 +11,7 @@ import {
PropsForExpirationTimer,
PropsForGroupInvitation,
PropsForGroupUpdate,
PropsForInteractionNotification,
} from '../../state/ducks/conversations';
import {
getOldBottomMessageId,
@ -28,6 +29,7 @@ import { useSelectedConversationKey } from '../../state/selectors/selectedConver
import { DataExtractionNotification } from './message/message-item/DataExtractionNotification';
import { SessionLastSeenIndicator } from './SessionLastSeenIndicator';
import { TimerNotification } from './TimerNotification';
import { InteractionNotification } from './message/message-item/InteractionNotification';
function isNotTextboxEvent(e: KeyboardEvent) {
return (e?.target as any)?.type === undefined;
@ -155,6 +157,12 @@ export const SessionMessagesList = (props: {
return [<CallNotification key={messageId} {...msgProps} />, ...componentToMerge];
}
if (messageProps.message?.messageType === 'interaction-notification') {
const msgProps = messageProps.message.props as PropsForInteractionNotification;
return [<InteractionNotification key={messageId} {...msgProps} />, ...componentToMerge];
}
if (!messageProps) {
return null;
}

@ -0,0 +1,76 @@
import React from 'react';
import { useIsPrivate, useIsPublic } from '../../../../hooks/useParamSelector';
import {
ConversationInteractionStatus,
ConversationInteractionType,
} from '../../../../interactions/conversationInteractions';
import styled from 'styled-components';
import { assertUnreachable } from '../../../../types/sqlSharedTypes';
import { isEmpty } from 'lodash';
import { Flex } from '../../../basic/Flex';
import { PropsForInteractionNotification } from '../../../../state/ducks/conversations';
import { ReadableMessage } from './ReadableMessage';
const StyledFailText = styled.div`
color: var(--danger-color);
`;
export const InteractionNotification = (props: PropsForInteractionNotification) => {
const { notificationType, convoId, messageId, receivedAt, isUnread } = props;
const { interactionStatus, interactionType } = notificationType;
const isGroup = !useIsPrivate(convoId);
const isCommunity = useIsPublic(convoId);
// NOTE For now we only show interaction errors in the message history
if (interactionStatus !== ConversationInteractionStatus.Error) {
return null;
}
let text = '';
switch (interactionType) {
case ConversationInteractionType.Hide:
text = window.i18n('hideConversationFailed');
break;
case ConversationInteractionType.Leave:
text = isCommunity
? window.i18n('leaveCommunityFailed')
: isGroup
? window.i18n('leaveGroupFailed')
: window.i18n('deleteConversationFailed');
break;
default:
assertUnreachable(
interactionType,
`InteractionErrorMessage: Missing case error "${interactionType}"`
);
}
if (isEmpty(text)) {
return null;
}
return (
<ReadableMessage
messageId={messageId}
receivedAt={receivedAt}
isUnread={isUnread}
key={`readable-message-${messageId}`}
>
<Flex
id={`convo-interaction-${convoId}`}
container={true}
flexDirection="row"
alignItems="center"
justifyContent="center"
margin={'var(--margins-md) var(--margins-sm)'}
data-testid="control-message"
>
<StyledFailText>{text}</StyledFailText>
</Flex>
</ReadableMessage>
);
};

@ -1,6 +1,7 @@
import {
ConversationNotificationSettingType,
ConversationTypeEnum,
READ_MESSAGE_STATE,
} from '../models/conversationAttributes';
import { CallManager, SyncUtils, ToastUtils, UserUtils } from '../session/utils';
@ -42,6 +43,7 @@ import { encryptProfile } from '../util/crypto/profileEncrypter';
import { ReleasedFeatures } from '../util/releaseFeature';
import { Storage, setLastProfileUpdateTimestamp } from '../util/storage';
import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface';
import { ConversationModel } from '../models/conversation';
export enum ConversationInteractionStatus {
Start = 'start',
@ -274,10 +276,11 @@ export function showLeavePrivateConversationbyConvoId(
await clearConversationInteractionState({ conversationId });
} catch (err) {
window.log.warn(`showLeavePrivateConversationbyConvoId error: ${err}`);
await updateConversationInteractionState({
await handleConversationInteractionError({
conversationId,
type: isMe ? ConversationInteractionType.Hide : ConversationInteractionType.Leave,
status: ConversationInteractionStatus.Error,
interactionType: isMe
? ConversationInteractionType.Hide
: ConversationInteractionType.Leave,
});
}
};
@ -339,10 +342,9 @@ export function showLeaveGroupByConvoId(conversationId: string, name: string | u
await clearConversationInteractionState({ conversationId });
} catch (err) {
window.log.warn(`showLeaveGroupByConvoId error: ${err}`);
await updateConversationInteractionState({
await handleConversationInteractionError({
conversationId,
type: ConversationInteractionType.Leave,
status: ConversationInteractionStatus.Error,
interactionType: ConversationInteractionType.Leave,
});
}
};
@ -734,3 +736,38 @@ export async function clearConversationInteractionState({
window.log.debug(`WIP: clearConversationInteractionState() for ${conversationId}`);
}
}
async function handleConversationInteractionError({
conversationId,
interactionType,
}: {
conversationId: string;
interactionType: ConversationInteractionType;
}) {
const conversation = getConversationController().get(conversationId);
if (!conversation) {
return;
}
const interactionStatus = ConversationInteractionStatus.Error;
await updateConversationInteractionState({
conversationId,
type: interactionType,
status: interactionStatus,
});
// Add an error message to the database so we can view it in the message history
await conversation?.addSingleIncomingMessage({
source: UserUtils.getOurPubKeyStrFromCache(),
sent_at: Date.now(),
interactionNotification: {
interactionType,
interactionStatus,
},
unread: READ_MESSAGE_STATE.read,
expireTimer: 0,
});
conversation.updateLastMessage();
}

@ -93,8 +93,12 @@ import { LinkPreviews } from '../util/linkPreviews';
import { Notifications } from '../util/notifications';
import { Storage } from '../util/storage';
import { ConversationModel } from './conversation';
import { roomHasBlindEnabled } from '../types/sqlSharedTypes';
import { assertUnreachable, roomHasBlindEnabled } from '../types/sqlSharedTypes';
import { READ_MESSAGE_STATE } from './conversationAttributes';
import {
ConversationInteractionStatus,
ConversationInteractionType,
} from '../interactions/conversationInteractions';
// tslint:disable: cyclomatic-complexity
/**
@ -145,6 +149,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
const propsForTimerNotification = this.getPropsForTimerNotification();
const propsForMessageRequestResponse = this.getPropsForMessageRequestResponse();
const callNotificationType = this.get('callNotificationType');
const interactionNotification = this.get('interactionNotification');
const messageProps: MessageModelPropsWithoutConvoProps = {
propsForMessage: this.getPropsForMessage(),
};
@ -172,6 +177,17 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
isUnread: this.isUnread(),
};
}
if (interactionNotification) {
messageProps.propsForInteractionNotification = {
notificationType: interactionNotification,
convoId: this.get('conversationId'),
messageId: this.id,
receivedAt: this.get('received_at') || Date.now(),
isUnread: this.isUnread(),
};
}
perfEnd(`getPropsMessage-${this.id}`, 'getPropsMessage');
return messageProps;
}
@ -434,7 +450,11 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
return undefined;
}
if (this.isDataExtractionNotification() || this.get('callNotificationType')) {
if (
this.isDataExtractionNotification() ||
this.get('callNotificationType') ||
this.get('interactionNotification')
) {
return undefined;
}
@ -1273,6 +1293,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
if (arrayContainsUsOnly(groupUpdate.kicked)) {
return window.i18n('youGotKickedFromGroup');
}
if (arrayContainsUsOnly(groupUpdate.left)) {
return window.i18n('youLeftTheGroup');
}
@ -1284,12 +1305,15 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
}
const messages = [];
if (!groupUpdate.name && !groupUpdate.joined && !groupUpdate.kicked && !groupUpdate.kicked) {
return window.i18n('updatedTheGroup'); // Group Updated
}
if (groupUpdate.name) {
return window.i18n('titleIsNow', [groupUpdate.name]);
}
if (groupUpdate.joined && groupUpdate.joined.length) {
const names = groupUpdate.joined.map(
getConversationController().getContactProfileNameOrShortenedPubKey
@ -1317,9 +1341,11 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
}
return messages.join(' ');
}
if (this.isIncoming() && this.hasErrors()) {
return window.i18n('incomingError');
}
if (this.isGroupInvitation()) {
return `😎 ${window.i18n('openGroupInvitation')}`;
}
@ -1338,6 +1364,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
getConversationController().getContactProfileNameOrShortenedPubKey(dataExtraction.source),
]);
}
if (this.get('callNotificationType')) {
const displayName = getConversationController().getContactProfileNameOrShortenedPubKey(
this.get('conversationId')
@ -1353,6 +1380,40 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
return window.i18n('answeredACall', [displayName]);
}
}
if (this.get('interactionNotification')) {
const interactionNotification = this.get('interactionNotification');
if (interactionNotification) {
const { interactionType, interactionStatus } = interactionNotification;
// NOTE For now we only show interaction errors in the message history
if (interactionStatus === ConversationInteractionStatus.Error) {
const convo = getConversationController().get(this.get('conversationId'));
if (convo) {
const isGroup = !convo.isPrivate();
const isCommunity = convo.isPublic();
switch (interactionType) {
case ConversationInteractionType.Hide:
return window.i18n('hideConversationFailed');
case ConversationInteractionType.Leave:
return isCommunity
? window.i18n('leaveCommunityFailed')
: isGroup
? window.i18n('leaveGroupFailed')
: window.i18n('deleteConversationFailed');
default:
assertUnreachable(
interactionType,
`Message.getDescription: Missing case error "${interactionType}"`
);
}
}
}
}
}
if (this.get('reaction')) {
const reaction = this.get('reaction');
if (reaction && reaction.emoji && reaction.emoji !== '') {

@ -2,6 +2,7 @@ import { defaultsDeep } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import {
CallNotificationType,
InteractionNotificationType,
LastMessageStatusType,
PropsForMessageWithConvoProps,
} from '../state/ducks/conversations';
@ -115,6 +116,12 @@ export interface MessageAttributes {
isDeleted?: boolean;
callNotificationType?: CallNotificationType;
/**
* This is used when a user has performed an interaction (hiding, leaving, etc.) on a conversation. At the moment, this is only used for showing interaction errors.
* Will 14/06/2023
*/
interactionNotification?: InteractionNotificationType;
}
export interface DataExtractionNotificationMsg {
@ -214,6 +221,7 @@ export interface MessageAttributesOptionals {
messageHash?: string;
isDeleted?: boolean;
callNotificationType?: CallNotificationType;
interactionNotification?: InteractionNotificationType;
}
/**

@ -37,6 +37,7 @@ export type MessageModelPropsWithoutConvoProps = {
propsForGroupUpdateMessage?: PropsForGroupUpdate;
propsForCallNotification?: PropsForCallNotification;
propsForMessageRequestResponse?: PropsForMessageRequestResponse;
propsForInteractionNotification?: PropsForInteractionNotification;
};
export type MessageModelPropsWithConvoProps = SortedMessageModelProps & {
@ -165,6 +166,14 @@ export type PropsForAttachment = {
} | null;
};
export type PropsForInteractionNotification = {
notificationType: InteractionNotificationType;
convoId: string;
messageId: string;
receivedAt: number;
isUnread: boolean;
};
export type PropsForMessageWithoutConvoProps = {
id: string; // messageId
direction: MessageModelType;
@ -222,6 +231,11 @@ export type LastMessageType = {
text: string | null;
};
export type InteractionNotificationType = {
interactionType: ConversationInteractionType;
interactionStatus: ConversationInteractionStatus;
};
/**
* This closely matches ConversationAttributes except making a lot of fields optional.
* The size of the redux store is an issue considering the number of conversations we have, so having optional fields here

@ -112,7 +112,8 @@ export type MessagePropsType =
| 'timer-notification'
| 'regular-message'
| 'unread-indicator'
| 'call-notification';
| 'call-notification'
| 'interaction-notification';
export const getSortedMessagesTypesOfSelectedConversation = createSelector(
getSortedMessagesOfSelectedConversation,
@ -202,6 +203,19 @@ export const getSortedMessagesTypesOfSelectedConversation = createSelector(
};
}
if (msg.propsForInteractionNotification) {
return {
...common,
message: {
messageType: 'interaction-notification',
props: {
...msg.propsForInteractionNotification,
messageId: msg.propsForMessage.id,
},
},
};
}
return {
showUnreadIndicator: isFirstUnread,
showDateBreak,

Loading…
Cancel
Save