diff --git a/src/script/conversation/ConversationCellState.ts b/src/script/conversation/ConversationCellState.ts
index 5059f037def..28d241e53ea 100644
--- a/src/script/conversation/ConversationCellState.ts
+++ b/src/script/conversation/ConversationCellState.ts
@@ -248,14 +248,9 @@ const _getStateGroupActivity = {
return '';
},
icon: (conversationEntity: Conversation): ConversationStatusIcon | void => {
- const lastMessageEntity = conversationEntity.getNewestMessage();
- const isMemberRemoval = lastMessageEntity.isMember() && (lastMessageEntity as MemberMessage).isMemberRemoval();
-
- if (isMemberRemoval) {
- return conversationEntity.showNotificationsEverything()
- ? ConversationStatusIcon.UNREAD_MESSAGES
- : ConversationStatusIcon.MUTED;
- }
+ return conversationEntity.showNotificationsEverything()
+ ? ConversationStatusIcon.UNREAD_MESSAGES
+ : ConversationStatusIcon.MUTED;
},
match: (conversationEntity: Conversation) => {
const lastMessageEntity = conversationEntity.getNewestMessage();
@@ -307,7 +302,7 @@ const _getStateRemoved = {
return '';
},
- icon: () => ConversationStatusIcon.NONE,
+ icon: () => ConversationStatusIcon.UNREAD_MESSAGES,
match: (conversationEntity: Conversation) => conversationEntity.removed_from_conversation(),
};
diff --git a/src/script/conversation/ConversationRepository.ts b/src/script/conversation/ConversationRepository.ts
index fc8a89dbe59..859020e3f6f 100644
--- a/src/script/conversation/ConversationRepository.ts
+++ b/src/script/conversation/ConversationRepository.ts
@@ -163,7 +163,6 @@ export enum CONVERSATION_READONLY_STATE {
export class ConversationRepository {
private isBlockingNotificationHandling: boolean;
- private readonly conversationsWithNewEvents: Map
;
private readonly ephemeralHandler: ConversationEphemeralHandler;
public readonly conversationLabelRepository: ConversationLabelRepository;
public readonly conversationRoleRepository: ConversationRoleRepository;
@@ -299,7 +298,6 @@ export class ConversationRepository {
}
this.isBlockingNotificationHandling = true;
- this.conversationsWithNewEvents = new Map();
this.teamState.isTeam.subscribe(() => this.mapGuestStatusSelf());
@@ -1979,9 +1977,6 @@ export class ConversationRepository {
const isFetchingFromStream = handlingState !== NOTIFICATION_HANDLING_STATE.WEB_SOCKET;
if (this.isBlockingNotificationHandling !== isFetchingFromStream) {
- if (!isFetchingFromStream) {
- this.checkChangedConversations();
- }
this.isBlockingNotificationHandling = isFetchingFromStream;
this.logger.info(`Block handling of conversation events: ${this.isBlockingNotificationHandling}`);
}
@@ -2481,16 +2476,6 @@ export class ConversationRepository {
this.onMemberUpdate(conversationEntity, response);
}
- private checkChangedConversations() {
- this.conversationsWithNewEvents.forEach(conversationEntity => {
- if (conversationEntity.shouldUnarchive()) {
- this.unarchiveConversation(conversationEntity, false, ConversationRepository.eventFromStreamMessage);
- }
- });
-
- this.conversationsWithNewEvents.clear();
- }
-
/**
* Clears conversation content from view and the database.
*
@@ -2685,19 +2670,10 @@ export class ConversationRepository {
const onEventPromise = isConversationCreate
? Promise.resolve(null)
: this.getConversationById(conversationId, true);
- let previouslyArchived = false;
return onEventPromise
.then((conversationEntity: Conversation) => {
if (conversationEntity) {
- // Check if conversation was archived
- previouslyArchived = conversationEntity.is_archived();
- const isPastMemberStatus = conversationEntity.status() === ConversationStatus.PAST_MEMBER;
- const isMemberJoinType = type === CONVERSATION_EVENT.MEMBER_JOIN;
-
- if (previouslyArchived && isPastMemberStatus && isMemberJoinType) {
- this.unarchiveConversation(conversationEntity, false, ConversationRepository.eventFromStreamMessage);
- }
const isBackendTimestamp = eventSource !== EventSource.INJECTED;
const eventsToSkip: (CLIENT_CONVERSATION_EVENT | CONVERSATION_EVENT)[] = [
@@ -2723,7 +2699,7 @@ export class ConversationRepository {
)
.then((entityObject = {} as EntityObject) => {
if (type !== CONVERSATION_EVENT.MEMBER_JOIN && type !== CONVERSATION_EVENT.MEMBER_LEAVE) {
- this.handleConversationNotification(entityObject as EntityObject, eventSource, previouslyArchived);
+ this.handleConversationNotification(entityObject as EntityObject, eventSource);
}
})
.catch((error: BaseError) => {
@@ -2961,14 +2937,9 @@ export class ConversationRepository {
*
* @param entityObject Object containing the conversation and the message that are targeted by the event
* @param eventSource Source of event
- * @param previouslyArchived `true` if the previous state of the conversation was archived
* @returns Resolves when the conversation was updated
*/
- private async handleConversationNotification(
- entityObject: EntityObject,
- eventSource: EventSource,
- previouslyArchived: boolean,
- ) {
+ private async handleConversationNotification(entityObject: EntityObject, eventSource: EventSource) {
const {conversationEntity, messageEntity} = entityObject;
if (conversationEntity) {
@@ -2990,18 +2961,6 @@ export class ConversationRepository {
conversationEntity.cleared_timestamp(0);
}
}
-
- // Check if event needs to be un-archived
- if (previouslyArchived) {
- // Add to check for un-archiving at the end of stream handling
- if (eventFromStream) {
- return this.conversationsWithNewEvents.set(conversationEntity.id, conversationEntity);
- }
-
- if (eventFromWebSocket && conversationEntity.shouldUnarchive()) {
- return this.unarchiveConversation(conversationEntity, false, 'event from WebSocket');
- }
- }
}
}
diff --git a/src/script/conversation/EventBuilder.ts b/src/script/conversation/EventBuilder.ts
index cec2dbe6688..09524d1ec02 100644
--- a/src/script/conversation/EventBuilder.ts
+++ b/src/script/conversation/EventBuilder.ts
@@ -57,7 +57,6 @@ export interface BaseEvent {
export interface ConversationEvent extends BaseEvent {
data: Data;
- id: string;
type: Type;
}
diff --git a/src/script/entity/Conversation.test.ts b/src/script/entity/Conversation.test.ts
index f92d259da63..83635207caf 100644
--- a/src/script/entity/Conversation.test.ts
+++ b/src/script/entity/Conversation.test.ts
@@ -21,22 +21,17 @@
import {ConnectionStatus} from '@wireapp/api-client/lib/connection/';
import {CONVERSATION_TYPE} from '@wireapp/api-client/lib/conversation/';
-import {CONVERSATION_EVENT} from '@wireapp/api-client/lib/event/';
import {ClientEntity} from 'src/script/client/ClientEntity';
import {ConnectionMapper} from 'src/script/connection/ConnectionMapper';
import {ConversationMapper} from 'src/script/conversation/ConversationMapper';
import {NOTIFICATION_STATE} from 'src/script/conversation/NotificationSetting';
import 'src/script/localization/Localizer';
-import {CALL_MESSAGE_TYPE} from 'src/script/message/CallMessageType';
-import {MentionEntity} from 'src/script/message/MentionEntity';
import {StatusType} from 'src/script/message/StatusType';
import {createUuid} from 'Util/uuid';
import {Conversation} from './Conversation';
-import {CallMessage} from './message/CallMessage';
import {ContentMessage} from './message/ContentMessage';
-import {MemberMessage} from './message/MemberMessage';
import {Message} from './message/Message';
import {PingMessage} from './message/PingMessage';
import {Text} from './message/Text';
@@ -897,158 +892,6 @@ describe('Conversation', () => {
});
});
- describe('shouldUnarchive', () => {
- let timestamp: number = undefined;
- let contentMessage: ContentMessage = undefined;
- let mutedTimestampMessage: PingMessage = undefined;
- let outdatedMessage: PingMessage = undefined;
- let pingMessage: PingMessage = undefined;
- let selfMentionMessage: ContentMessage = undefined;
- const conversationEntity = new Conversation(createUuid());
-
- const selfUserEntity = new User(createUuid(), null);
- selfUserEntity.isMe = true;
- selfUserEntity.inTeam(true);
- conversationEntity.selfUser(selfUserEntity);
-
- beforeEach(() => {
- timestamp = Date.now();
- conversationEntity.archivedTimestamp(timestamp);
- conversationEntity.archivedState(true);
-
- mutedTimestampMessage = new PingMessage();
- mutedTimestampMessage.timestamp(timestamp);
-
- outdatedMessage = new PingMessage();
- outdatedMessage.timestamp(timestamp - 100);
-
- contentMessage = new ContentMessage();
- contentMessage.assets([new Text('id', 'Hello there')]);
- contentMessage.timestamp(timestamp + 100);
-
- pingMessage = new PingMessage();
- pingMessage.timestamp(timestamp + 200);
-
- selfMentionMessage = new ContentMessage();
- const mentionEntity = new MentionEntity(0, 7, selfUserEntity.id, selfUserEntity.domain);
- const textAsset = new Text('id', '@Gregor, Hello there');
- textAsset.mentions.push(mentionEntity);
- selfMentionMessage.assets([textAsset]);
- selfMentionMessage.timestamp(timestamp + 300);
- });
-
- afterEach(() => conversationEntity.messages_unordered.removeAll());
-
- it('returns false if conversation is not archived', () => {
- conversationEntity.archivedState(false);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(outdatedMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(mutedTimestampMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(contentMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(pingMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(selfMentionMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- });
-
- it('returns false if conversation is in no notification state', () => {
- conversationEntity.mutedState(NOTIFICATION_STATE.NOTHING);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(outdatedMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(mutedTimestampMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(contentMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(pingMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(selfMentionMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- });
-
- it('returns expected value if conversation is in only mentions notifications state', () => {
- conversationEntity.mutedState(NOTIFICATION_STATE.MENTIONS_AND_REPLIES);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(outdatedMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(mutedTimestampMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(contentMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(pingMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(selfMentionMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(true);
- });
-
- it('returns expected value if conversation is in everything notifications state', () => {
- conversationEntity.mutedState(NOTIFICATION_STATE.EVERYTHING);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(outdatedMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(mutedTimestampMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- conversationEntity.messages_unordered.push(contentMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(true);
- conversationEntity.messages_unordered.removeAll();
-
- const memberLeaveMessage = new MemberMessage();
- memberLeaveMessage.type = CONVERSATION_EVENT.MEMBER_LEAVE;
- memberLeaveMessage.timestamp(timestamp + 100);
- conversationEntity.messages_unordered.push(memberLeaveMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
-
- const callMessage = new CallMessage(CALL_MESSAGE_TYPE.ACTIVATED);
- callMessage.timestamp(timestamp + 200);
- conversationEntity.messages_unordered.push(callMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(true);
- conversationEntity.messages_unordered.removeAll();
- conversationEntity.messages_unordered.push(memberLeaveMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- const memberJoinMessage = new MemberMessage();
- memberJoinMessage.type = CONVERSATION_EVENT.MEMBER_JOIN;
- memberJoinMessage.timestamp(timestamp + 200);
- conversationEntity.messages_unordered.push(memberJoinMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(false);
- const selfJoinMessage = new MemberMessage();
- selfJoinMessage.type = CONVERSATION_EVENT.MEMBER_JOIN;
- selfJoinMessage.userIds.push({domain: selfUserEntity.domain, id: selfUserEntity.id});
- selfJoinMessage.timestamp(timestamp + 200);
- conversationEntity.messages_unordered.push(selfJoinMessage);
-
- expect(conversationEntity.shouldUnarchive()).toBe(true);
- });
- });
-
describe('_incrementTimeOnly', () => {
it('should update only to newer timestamps', () => {
//@ts-ignore
diff --git a/src/script/entity/Conversation.ts b/src/script/entity/Conversation.ts
index 75361c8f4ab..849dd2f2765 100644
--- a/src/script/entity/Conversation.ts
+++ b/src/script/entity/Conversation.ts
@@ -42,7 +42,6 @@ import {truncate} from 'Util/StringUtil';
import {CallMessage} from './message/CallMessage';
import type {ContentMessage} from './message/ContentMessage';
-import type {MemberMessage} from './message/MemberMessage';
import type {Message} from './message/Message';
import {PingMessage} from './message/PingMessage';
import type {User} from './User';
@@ -831,38 +830,6 @@ export class Conversation {
this.messages_unordered.removeAll();
}
- shouldUnarchive(): boolean {
- if (!this.archivedState() || this.showNotificationsNothing()) {
- return false;
- }
-
- const isNewerMessage = (messageEntity: Message) => messageEntity.timestamp() > this.archivedTimestamp();
-
- const {allEvents, allMessages, selfMentions, selfReplies} = this.unreadState();
- if (this.showNotificationsMentionsAndReplies()) {
- const mentionsAndReplies = selfMentions.concat(selfReplies);
- return mentionsAndReplies.some(isNewerMessage);
- }
-
- const hasNewMessage = allMessages.some(isNewerMessage);
- if (hasNewMessage) {
- return true;
- }
-
- return allEvents.some(messageEntity => {
- if (!isNewerMessage(messageEntity)) {
- return false;
- }
-
- const isCallActivation = messageEntity.isCall() && messageEntity.isActivation();
- const isMemberJoin = messageEntity.isMember() && (messageEntity as MemberMessage).isMemberJoin();
- const wasSelfUserAdded =
- isMemberJoin && (messageEntity as MemberMessage).isUserAffected(this.selfUser().qualifiedId);
-
- return isCallActivation || wasSelfUserAdded;
- });
- }
-
/**
* Checks for message duplicates.
*
diff --git a/src/script/event/EventRepository.test.ts b/src/script/event/EventRepository.test.ts
index 49d8d4aea8e..c352f3dea77 100644
--- a/src/script/event/EventRepository.test.ts
+++ b/src/script/event/EventRepository.test.ts
@@ -19,23 +19,14 @@
import {BackendEvent, CONVERSATION_EVENT, USER_EVENT} from '@wireapp/api-client/lib/event/';
-import {Asset as ProtobufAsset} from '@wireapp/protocol-messaging';
-
-import {AssetTransferState} from 'src/script/assets/AssetTransferState';
-import {EventError} from 'src/script/error/EventError';
import {ClientEvent} from 'src/script/event/Client';
import {EventRepository} from 'src/script/event/EventRepository';
import {NOTIFICATION_HANDLING_STATE} from 'src/script/event/NotificationHandlingState';
-import {createUuid} from 'Util/uuid';
-import {EventService} from './EventService';
import {EventSource} from './EventSource';
import {TestFactory} from '../../../test/helper/TestFactory';
import {ClientConversationEvent} from '../conversation/EventBuilder';
-import {User} from '../entity/User';
-import {StatusType} from '../message/StatusType';
-import {EventRecord} from '../storage';
const testFactory = new TestFactory();
@@ -46,64 +37,9 @@ describe('EventRepository', () => {
return testFactory.exposeEventActors();
});
- describe('getCommonMessageUpdates', () => {
- /** @see https://wearezeta.atlassian.net/browse/SQCORE-732 */
- it('does not overwrite the seen status if a message gets edited', () => {
- const originalEvent = {
- category: 16,
- conversation: 'a7f1187e-9396-44c9-8242-db9d3051dc89',
- data: {
- content: 'Original Text Which Has Been Seen By Someone Else',
- expects_read_confirmation: true,
- legal_hold_status: 1,
- mentions: [],
- previews: [],
- },
- from: '24de8432-03ba-439f-88f8-95bdc68b7bdd',
- from_client_id: '79618bbe93e6821c',
- id: 'c6269e58-fa82-4f6e-8264-263e09154871',
- primary_key: '17',
- read_receipts: [
- {
- time: '2021-06-10T19:47:19.570Z',
- userId: 'b661e27f-24c6-4c52-a425-87a7b7f3df61',
- },
- ],
- status: StatusType.SEEN,
- time: '2021-06-10T19:47:16.071Z',
- type: ClientEvent.CONVERSATION.MESSAGE_ADD,
- } as any;
-
- const editedEvent = {
- conversation: 'a7f1187e-9396-44c9-8242-db9d3051dc89',
- data: {
- content: 'Edited Text Which Replaces The Original Text',
- expects_read_confirmation: true,
- mentions: [],
- previews: [],
- replacing_message_id: 'c6269e58-fa82-4f6e-8264-263e09154871',
- },
- from: '24de8432-03ba-439f-88f8-95bdc68b7bdd',
- from_client_id: '79618bbe93e6821c',
- id: 'caff044b-cb9c-47c6-833a-d4b76c678bcd',
- status: StatusType.SENT,
- time: '2021-06-10T19:47:23.706Z',
- type: ClientEvent.CONVERSATION.MESSAGE_ADD,
- } as any;
-
- const updatedEvent = EventRepository['getCommonMessageUpdates'](originalEvent, editedEvent);
- expect(updatedEvent.data.content).toBe('Edited Text Which Replaces The Original Text');
- expect(updatedEvent.status).toBe(StatusType.SEEN);
- expect(Object.keys(updatedEvent.read_receipts).length).toBe(1);
- });
- });
-
describe('handleEvent', () => {
beforeEach(() => {
testFactory.event_repository!.notificationHandlingState(NOTIFICATION_HANDLING_STATE.WEB_SOCKET);
- jest
- .spyOn(testFactory.event_service!, 'saveEvent')
- .mockReturnValue(Promise.resolve({data: 'dummy content'} as EventRecord));
spyOn(testFactory.event_repository!, 'distributeEvent');
});
@@ -114,7 +50,6 @@ describe('EventRepository', () => {
EventSource.NOTIFICATION_STREAM,
)
.then(() => {
- expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled();
expect(testFactory.event_repository!['distributeEvent']).toHaveBeenCalled();
});
});
@@ -126,7 +61,6 @@ describe('EventRepository', () => {
EventSource.NOTIFICATION_STREAM,
)
.then(() => {
- expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled();
expect(testFactory.event_repository!['distributeEvent']).toHaveBeenCalled();
});
});
@@ -138,7 +72,6 @@ describe('EventRepository', () => {
EventSource.NOTIFICATION_STREAM,
)
.then(() => {
- expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled();
expect(testFactory.event_repository!['distributeEvent']).toHaveBeenCalled();
});
});
@@ -154,7 +87,6 @@ describe('EventRepository', () => {
} as BackendEvent;
return testFactory.event_repository!['handleEvent']({event}, EventSource.NOTIFICATION_STREAM).then(() => {
- expect(testFactory.event_service!.saveEvent).toHaveBeenCalled();
expect(testFactory.event_repository!['distributeEvent']).toHaveBeenCalled();
});
});
@@ -170,7 +102,6 @@ describe('EventRepository', () => {
} as BackendEvent;
return testFactory.event_repository!['handleEvent']({event}, EventSource.NOTIFICATION_STREAM).then(() => {
- expect(testFactory.event_service!.saveEvent).toHaveBeenCalled();
expect(testFactory.event_repository!['distributeEvent']).toHaveBeenCalled();
});
});
@@ -186,18 +117,13 @@ describe('EventRepository', () => {
} as BackendEvent;
return testFactory.event_repository!['handleEvent']({event}, EventSource.NOTIFICATION_STREAM).then(() => {
- expect(testFactory.event_service!.saveEvent).toHaveBeenCalled();
expect(testFactory.event_repository!['distributeEvent']).toHaveBeenCalled();
});
});
it('accepts "conversation.voice-channel-deactivate" (missed call) events', async () => {
- const eventServiceSpy = {
- loadEvent: jest.fn().mockResolvedValue(undefined),
- saveEvent: jest.fn().mockResolvedValue({data: 'dummy content'}),
- } as unknown as EventService;
const fakeProp: any = undefined;
- const eventRepo = new EventRepository(eventServiceSpy, fakeProp, fakeProp, fakeProp);
+ const eventRepo = new EventRepository({} as any, fakeProp, fakeProp, fakeProp);
eventRepo.notificationHandlingState(NOTIFICATION_HANDLING_STATE.WEB_SOCKET);
jest.spyOn(eventRepo, 'distributeEvent').mockImplementation(() => {});
@@ -212,7 +138,6 @@ describe('EventRepository', () => {
await eventRepo['handleEvent']({event}, EventSource.NOTIFICATION_STREAM);
- expect(eventServiceSpy.saveEvent).toHaveBeenCalled();
expect(eventRepo['distributeEvent']).toHaveBeenCalled();
});
@@ -228,383 +153,8 @@ describe('EventRepository', () => {
} as ClientConversationEvent;
return testFactory.event_repository.injectEvent(event).then(() => {
- expect(testFactory.event_service!.saveEvent).toHaveBeenCalled();
expect(testFactory.event_repository!['distributeEvent']).toHaveBeenCalled();
});
});
});
-
- describe('processEvent', () => {
- let event: any;
- let previously_stored_event: any;
-
- beforeEach(() => {
- event = {
- conversation: createUuid(),
- data: {
- content: 'Lorem Ipsum',
- previews: [],
- },
- from: createUuid(),
- id: createUuid(),
- time: new Date().toISOString(),
- type: ClientEvent.CONVERSATION.MESSAGE_ADD,
- };
-
- jest
- .spyOn(testFactory.event_service!, 'saveEvent')
- .mockImplementation(saved_event => Promise.resolve(saved_event as any));
- });
-
- it('saves an event with a previously not used ID', () => {
- jest.spyOn(testFactory.event_service!, 'loadEvent').mockClear();
-
- return testFactory.event_repository!['processEvent'](event, EventSource.NOTIFICATION_STREAM).then(() => {
- expect(testFactory.event_service!.saveEvent).toHaveBeenCalled();
- });
- });
-
- it('ignores an event with an ID previously used by another user', () => {
- previously_stored_event = JSON.parse(JSON.stringify(event));
- previously_stored_event.from = createUuid();
- jest
- .spyOn(testFactory.event_service!, 'loadEvent')
- .mockImplementation(() => Promise.resolve(previously_stored_event));
-
- return testFactory
- .event_repository!['processEvent'](event, EventSource.NOTIFICATION_STREAM)
- .then(() => fail('Method should have thrown an error'))
- .catch(error => {
- expect(error).toEqual(jasmine.any(EventError));
- expect(error.type).toBe(EventError.TYPE.VALIDATION_FAILED);
- expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled();
- });
- });
-
- it('ignores a non-"text message" with an ID previously used by the same user', () => {
- event.type = ClientEvent.CALL.E_CALL;
- previously_stored_event = JSON.parse(JSON.stringify(event));
- jest
- .spyOn(testFactory.event_service!, 'loadEvent')
- .mockImplementation(() => Promise.resolve(previously_stored_event));
-
- return testFactory
- .event_repository!['handleEventSaving'](event)
- .then(() => fail('Method should have thrown an error'))
- .catch(error => {
- expect(error).toEqual(jasmine.any(EventError));
- expect(error.type).toBe(EventError.TYPE.VALIDATION_FAILED);
- expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled();
- });
- });
-
- it('ignores a plain text message with an ID previously used by the same user for a non-"text message"', () => {
- previously_stored_event = JSON.parse(JSON.stringify(event));
- previously_stored_event.type = ClientEvent.CALL.E_CALL;
- jest
- .spyOn(testFactory.event_service!, 'loadEvent')
- .mockImplementation(() => Promise.resolve(previously_stored_event));
-
- return testFactory
- .event_repository!['processEvent'](event, EventSource.NOTIFICATION_STREAM)
- .then(() => fail('Method should have thrown an error'))
- .catch(error => {
- expect(error).toEqual(jasmine.any(EventError));
- expect(error.type).toBe(EventError.TYPE.VALIDATION_FAILED);
- expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled();
- });
- });
-
- it('ignores a plain text message with an ID previously used by the same user', () => {
- previously_stored_event = JSON.parse(JSON.stringify(event));
- jest
- .spyOn(testFactory.event_service!, 'loadEvent')
- .mockImplementation(() => Promise.resolve(previously_stored_event));
-
- return testFactory
- .event_repository!['processEvent'](event, EventSource.NOTIFICATION_STREAM)
- .then(() => fail('Method should have thrown an error'))
- .catch(error => {
- expect(error).toEqual(jasmine.any(EventError));
- expect(error.type).toBe(EventError.TYPE.VALIDATION_FAILED);
- expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled();
- });
- });
-
- it('ignores a text message with link preview with an ID previously used by the same user for a text message with link preview', () => {
- event.data.previews.push(1);
- previously_stored_event = JSON.parse(JSON.stringify(event));
- jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(previously_stored_event as any);
-
- return testFactory
- .event_repository!['processEvent'](event, EventSource.NOTIFICATION_STREAM)
- .then(() => fail('Method should have thrown an error'))
- .catch(error => {
- expect(error).toEqual(jasmine.any(EventError));
- expect(error.type).toBe(EventError.TYPE.VALIDATION_FAILED);
- expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled();
- });
- });
-
- it('ignores a text message with link preview with an ID previously used by the same user for a text message different content', () => {
- previously_stored_event = JSON.parse(JSON.stringify(event));
- jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(previously_stored_event as any);
-
- event.data.previews.push(1);
- event.data.content = 'Ipsum loren';
-
- return testFactory
- .event_repository!['processEvent'](event, EventSource.NOTIFICATION_STREAM)
- .then(() => fail('Method should have thrown an error'))
- .catch(error => {
- expect(error).toEqual(jasmine.any(EventError));
- expect(error.type).toBe(EventError.TYPE.VALIDATION_FAILED);
- expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled();
- });
- });
-
- it('saves a text message with link preview with an ID previously used by the same user for a plain text message', () => {
- previously_stored_event = JSON.parse(JSON.stringify(event));
- jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(previously_stored_event);
- jest
- .spyOn(testFactory.event_service!, 'replaceEvent')
- .mockImplementation(() => Promise.resolve(previously_stored_event));
-
- const initial_time = event.time;
- const changed_time = new Date(new Date(event.time).getTime() + 60 * 1000).toISOString();
- event.data.previews.push(1);
- event.time = changed_time;
-
- return testFactory
- .event_repository!['processEvent'](event, EventSource.NOTIFICATION_STREAM)
- .then((saved_event: any) => {
- expect(saved_event.time).toEqual(initial_time);
- expect(saved_event.time).not.toEqual(changed_time);
- expect(saved_event.primary_key).toEqual(previously_stored_event.primary_key);
- expect(testFactory.event_service!.replaceEvent).toHaveBeenCalled();
- });
- });
-
- it('ignores edit message with missing associated original message', () => {
- const linkPreviewEvent = JSON.parse(JSON.stringify(event));
- jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue({} as any);
- jest
- .spyOn(testFactory.event_service!, 'replaceEvent')
- .mockImplementation(() => Promise.resolve({} as EventRecord));
-
- linkPreviewEvent.data.replacing_message_id = 'initial_message_id';
-
- return testFactory
- .event_repository!['handleEventSaving'](linkPreviewEvent)
- .then(() => fail('Should have thrown an error'))
- .catch(() => {
- expect(testFactory.event_service!.replaceEvent).not.toHaveBeenCalled();
- expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled();
- });
- });
-
- it('updates edited messages when link preview arrives', () => {
- const replacingId = 'old-replaced-message-id';
- const storedEvent = {
- ...event,
- data: {...event.data, replacing_message_id: replacingId},
- } as EventRecord;
- const linkPreviewEvent = {...event};
- jest
- .spyOn(testFactory.event_service!, 'loadEvent')
- .mockImplementation((conversationId: string, messageId: string) => {
- return messageId === replacingId ? Promise.resolve(undefined) : Promise.resolve(storedEvent as any);
- });
- jest
- .spyOn(testFactory.event_service!, 'replaceEvent')
- .mockImplementation((ev: EventRecord) => Promise.resolve(ev));
-
- linkPreviewEvent.data.replacing_message_id = replacingId;
- linkPreviewEvent.data.previews = ['preview'];
-
- return testFactory.event_repository!['handleEventSaving'](linkPreviewEvent).then((updatedEvent: any) => {
- expect(testFactory.event_service!.replaceEvent).toHaveBeenCalled();
- expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled();
- expect(updatedEvent?.data.previews[0]).toEqual('preview');
- });
- });
-
- it('updates edited messages', () => {
- const originalMessage = JSON.parse(JSON.stringify(event));
- originalMessage.reactions = ['user-id'];
- jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(originalMessage as any);
- jest
- .spyOn(testFactory.event_service!, 'replaceEvent')
- .mockImplementation((updates: EventRecord) => Promise.resolve(updates));
-
- const initial_time = event.time;
- const changed_time = new Date(new Date(event.time).getTime() + 60 * 1000).toISOString();
- originalMessage.primary_key = 12;
- event.id = createUuid();
- event.data.content = 'new content';
- event.data.replacing_message_id = originalMessage.id;
- event.time = changed_time;
-
- return testFactory.event_repository!['handleEventSaving'](event).then((updatedEvent: any) => {
- expect(updatedEvent.time).toEqual(initial_time);
- expect(updatedEvent.time).not.toEqual(changed_time);
- expect(updatedEvent.data.content).toEqual('new content');
- expect(updatedEvent.primary_key).toEqual(originalMessage.primary_key);
- expect(Object.keys(updatedEvent.reactions).length).toEqual(0);
- expect(testFactory.event_service!.replaceEvent).toHaveBeenCalled();
- });
- });
-
- it('updates link preview when edited', () => {
- const replacingId = 'replaced-message-id';
- const storedEvent = {
- ...event,
- data: {...event.data, previews: ['preview']},
- } as any;
- const editEvent = {...event} as any;
- jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(storedEvent as any);
- jest
- .spyOn(testFactory.event_service!, 'replaceEvent')
- .mockImplementation((ev: EventRecord) => Promise.resolve(ev));
-
- editEvent.data.replacing_message_id = replacingId;
-
- return testFactory.event_repository!['handleEventSaving'](editEvent).then((updatedEvent: any) => {
- expect(testFactory.event_service!.replaceEvent).toHaveBeenCalled();
- expect(testFactory.event_service!.saveEvent).not.toHaveBeenCalled();
- expect(updatedEvent?.data.previews.length).toEqual(0);
- });
- });
-
- it('saves a conversation.asset-add event', () => {
- const assetAddEvent = {...event, type: ClientEvent.CONVERSATION.ASSET_ADD};
-
- jest.spyOn(testFactory.event_service!, 'loadEvent').mockClear();
-
- return testFactory
- .event_repository!['processEvent'](assetAddEvent, EventSource.NOTIFICATION_STREAM)
- .then(updatedEvent => {
- expect(updatedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD);
- expect(testFactory.event_service!.saveEvent).toHaveBeenCalled();
- });
- });
-
- it('deletes cancelled conversation.asset-add event', async () => {
- const fromIds = [
- // cancel from an other user
- createUuid(),
- // cancel from the self user
- testFactory.user_repository['userState'].self().id,
- ];
-
- const loadEventSpy = jest.spyOn(testFactory.event_service!, 'loadEvent');
- const deleteEventSpy = jest.spyOn(testFactory.event_service!, 'deleteEvent');
- for (const fromId of fromIds) {
- const assetAddEvent = {...event, from: fromId, type: ClientEvent.CONVERSATION.ASSET_ADD};
- const assetCancelEvent = {
- ...assetAddEvent,
- data: {reason: ProtobufAsset.NotUploaded.CANCELLED, status: AssetTransferState.UPLOAD_FAILED},
- time: '2017-09-06T09:43:36.528Z',
- };
-
- loadEventSpy.mockResolvedValue(assetAddEvent as any);
- deleteEventSpy.mockImplementation(() => Promise.resolve(1));
-
- const savedEvent = await testFactory.event_repository!['processEvent'](
- assetCancelEvent,
- EventSource.NOTIFICATION_STREAM,
- );
- expect(savedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD);
- expect(testFactory.event_service!.deleteEvent).toHaveBeenCalled();
- }
- });
-
- it('deletes other user failed upload for conversation.asset-add event', () => {
- const assetAddEvent = {...event, type: ClientEvent.CONVERSATION.ASSET_ADD};
- const assetUploadFailedEvent = {
- ...assetAddEvent,
- data: {reason: ProtobufAsset.NotUploaded.FAILED, status: AssetTransferState.UPLOAD_FAILED},
- time: '2017-09-06T09:43:36.528Z',
- };
-
- jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(assetAddEvent as any);
- jest.spyOn(testFactory.event_service!, 'deleteEvent').mockImplementation(() => Promise.resolve(1));
-
- return testFactory
- .event_repository!['processEvent'](assetUploadFailedEvent, EventSource.NOTIFICATION_STREAM)
- .then(savedEvent => {
- expect(savedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD);
- expect(testFactory.event_service!.deleteEvent).toHaveBeenCalled();
- });
- });
-
- it('updates self failed upload for conversation.asset-add event', async () => {
- const assetAddEvent: EventRecord = {...event, type: ClientEvent.CONVERSATION.ASSET_ADD};
- const assetUploadFailedEvent = {
- ...assetAddEvent,
- data: {reason: ProtobufAsset.NotUploaded.FAILED, status: AssetTransferState.UPLOAD_FAILED},
- time: '2017-09-06T09:43:36.528Z',
- } as any;
-
- jest
- .spyOn(testFactory.event_repository!['userState'], 'self')
- .mockReturnValue(new User(assetAddEvent.from) as any);
- jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(assetAddEvent as any);
- jest
- .spyOn(testFactory.event_service!, 'updateEventAsUploadFailed')
- .mockImplementation(() => Promise.resolve(assetUploadFailedEvent));
-
- const savedEvent = await testFactory.event_repository!['processEvent'](
- assetUploadFailedEvent,
- EventSource.NOTIFICATION_STREAM,
- );
- expect(savedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD);
- expect(testFactory.event_service!.updateEventAsUploadFailed).toHaveBeenCalled();
- });
-
- it('handles conversation.asset-add state update event', () => {
- const initialAssetEvent = {...event, type: ClientEvent.CONVERSATION.ASSET_ADD};
-
- const updateStatusEvent = {
- ...initialAssetEvent,
- data: {status: AssetTransferState.UPLOADED},
- time: '2017-09-06T09:43:36.528Z',
- };
-
- jest
- .spyOn(testFactory.event_service!, 'replaceEvent')
- .mockImplementation(eventToUpdate => Promise.resolve(eventToUpdate));
- jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(initialAssetEvent as any);
-
- return testFactory
- .event_repository!['processEvent'](updateStatusEvent, EventSource.NOTIFICATION_STREAM)
- .then((updatedEvent: any) => {
- expect(updatedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD);
- expect(updatedEvent.data.status).toEqual(updateStatusEvent.data.status);
- expect(testFactory.event_service!.replaceEvent).toHaveBeenCalled();
- });
- });
-
- it('updates video when preview is received', () => {
- const initialAssetEvent = {...event, type: ClientEvent.CONVERSATION.ASSET_ADD};
-
- const AssetPreviewEvent = {
- ...initialAssetEvent,
- data: {status: AssetTransferState.UPLOADED},
- time: '2017-09-06T09:43:36.528Z',
- };
-
- jest
- .spyOn(testFactory.event_service!, 'replaceEvent')
- .mockImplementation(eventToUpdate => Promise.resolve(eventToUpdate));
- jest.spyOn(testFactory.event_service!, 'loadEvent').mockResolvedValue(initialAssetEvent as any);
-
- return testFactory
- .event_repository!['processEvent'](AssetPreviewEvent, EventSource.NOTIFICATION_STREAM)
- .then((updatedEvent: EventRecord) => {
- expect(updatedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD);
- expect(testFactory.event_service!.replaceEvent).toHaveBeenCalled();
- });
- });
- });
});
diff --git a/src/script/event/EventRepository.ts b/src/script/event/EventRepository.ts
index 9e348ddf844..175c18e804b 100644
--- a/src/script/event/EventRepository.ts
+++ b/src/script/event/EventRepository.ts
@@ -29,32 +29,26 @@ import ko from 'knockout';
import {container} from 'tsyringe';
import {Account, ConnectionState, ProcessedEventPayload} from '@wireapp/core';
-import {Asset as ProtobufAsset} from '@wireapp/protocol-messaging';
import {WebAppEvents} from '@wireapp/webapp-events';
import {getLogger, Logger} from 'Util/Logger';
import {queue} from 'Util/PromiseQueue';
import {TIME_IN_MILLIS} from 'Util/TimeUtil';
-import {ClientEvent, CONVERSATION} from './Client';
+import {ClientEvent} from './Client';
import {EventMiddleware, EventProcessor, IncomingEvent} from './EventProcessor';
import type {EventService} from './EventService';
import {EventSource} from './EventSource';
import {EVENT_TYPE} from './EventType';
-import {EventTypeHandling} from './EventTypeHandling';
import {EventValidation} from './EventValidation';
import {validateEvent} from './EventValidator';
import {NOTIFICATION_HANDLING_STATE} from './NotificationHandlingState';
import type {NotificationService} from './NotificationService';
-import {AssetTransferState} from '../assets/AssetTransferState';
-import {AssetAddEvent, ClientConversationEvent, EventBuilder, MessageAddEvent} from '../conversation/EventBuilder';
+import {ClientConversationEvent, EventBuilder} from '../conversation/EventBuilder';
import {CryptographyMapper} from '../cryptography/CryptographyMapper';
import {CryptographyError} from '../error/CryptographyError';
import {EventError} from '../error/EventError';
-import {categoryFromEvent} from '../message/MessageCategorization';
-import {isEventRecordFailed, isEventRecordWithFederationError} from '../message/StatusType';
-import type {EventRecord, StoredEvent} from '../storage';
import type {ServerTimeHandler} from '../time/serverTimeHandler';
import {EventName} from '../tracking/EventName';
import {UserState} from '../user/UserState';
@@ -62,14 +56,11 @@ import {Warnings} from '../view_model/WarningsContainer';
export class EventRepository {
logger: Logger;
- notificationHandlingState: ko.Observable;
- previousHandlingState: NOTIFICATION_HANDLING_STATE | undefined;
- notificationsHandled: number;
- notificationsTotal: number;
- lastEventDate: ko.Observable;
- eventProcessMiddlewares: EventMiddleware[] = [];
+ notificationHandlingState = ko.observable(NOTIFICATION_HANDLING_STATE.STREAM);
+ private readonly lastEventDate: ko.Observable = ko.observable();
+ private eventProcessMiddlewares: EventMiddleware[] = [];
/** event processors are classes that are able to react and process an incoming event */
- eventProcessors: EventProcessor[] = [];
+ private eventProcessors: EventProcessor[] = [];
static get CONFIG() {
return {
@@ -108,14 +99,9 @@ export class EventRepository {
) {
this.logger = getLogger('EventRepository');
- this.notificationHandlingState = ko.observable(NOTIFICATION_HANDLING_STATE.STREAM);
this.notificationHandlingState.subscribe(handling_state => {
amplify.publish(WebAppEvents.EVENT.NOTIFICATION_HANDLING_STATE, handling_state);
});
- this.notificationsHandled = 0;
- this.notificationsTotal = 0;
-
- this.lastEventDate = ko.observable();
}
/**
@@ -430,12 +416,6 @@ export class EventRepository {
event = await eventProcessMiddleware.processEvent(event);
}
- const shouldSaveEvent = EventTypeHandling.STORE.includes(event.type as CONVERSATION_EVENT);
- if (shouldSaveEvent) {
- const savedEvent = await this.handleEventSaving(event);
- event = savedEvent ?? event;
- }
-
return this.handleEventDistribution(event, source);
}
@@ -463,211 +443,4 @@ export class EventRepository {
}
return this.distributeEvent(event, source);
}
-
- /**
- * Handle a mapped event, check for malicious ID use and save it.
- *
- * @param event Backend event extracted from notification stream
- * @returns Resolves with the saved event
- */
- private async handleEventSaving(event: IncomingEvent) {
- const conversationId = 'conversation' in event && event.conversation;
-
- // first check if a message that should be replaced exists in DB
- if (event.type === ClientEvent.CONVERSATION.MESSAGE_ADD) {
- const mappedData = event.data;
- const eventToReplace = mappedData.replacing_message_id
- ? await this.eventService.loadEvent(conversationId, mappedData.replacing_message_id)
- : undefined;
-
- const hasLinkPreview = mappedData.previews && mappedData.previews.length;
- const isReplacementWithoutOriginal = !eventToReplace && mappedData.replacing_message_id;
- if (isReplacementWithoutOriginal && !hasLinkPreview) {
- // the only valid case of a replacement with no original message is when an edited message gets a link preview
- this.throwValidationError(event, 'Edit event without original event');
- }
-
- if (eventToReplace?.type === CONVERSATION.MESSAGE_ADD) {
- return this.handleEventReplacement(eventToReplace, event);
- }
- }
-
- // check for duplicates (same id)
- const storedEvent = 'id' in event ? await this.eventService.loadEvent(conversationId, event.id) : undefined;
-
- return storedEvent
- ? this.handleDuplicatedEvent(storedEvent, event)
- : this.eventService.saveEvent(event as EventRecord);
- }
-
- private handleEventReplacement(originalEvent: StoredEvent, newEvent: MessageAddEvent) {
- if (originalEvent.from !== newEvent.from) {
- const logMessage = `ID previously used by user '${newEvent.from}'`;
- const errorMessage = 'ID reused by other user';
- this.throwValidationError(newEvent, errorMessage, logMessage);
- }
- const newData = newEvent.data;
- const primaryKeyUpdate = {primary_key: originalEvent.primary_key};
- const isLinkPreviewEdit = newData?.previews && !!newData?.previews.length;
-
- const commonUpdates = EventRepository.getCommonMessageUpdates(originalEvent, newEvent);
-
- const specificUpdates = isLinkPreviewEdit
- ? this.getUpdatesForMessage(originalEvent, newEvent)
- : EventRepository.getUpdatesForEditMessage(originalEvent, newEvent);
-
- const updates = {...specificUpdates, ...commonUpdates};
-
- const identifiedUpdates = {...primaryKeyUpdate, ...updates};
- return this.eventService.replaceEvent(identifiedUpdates);
- }
-
- private handleDuplicatedEvent(originalEvent: EventRecord, newEvent: IncomingEvent) {
- switch (newEvent.type) {
- case ClientEvent.CONVERSATION.ASSET_ADD:
- return this.handleAssetUpdate(originalEvent, newEvent);
-
- case ClientEvent.CONVERSATION.MESSAGE_ADD:
- return this.handleMessageUpdate(originalEvent, newEvent);
-
- default:
- this.throwValidationError(newEvent, `Forbidden type '${newEvent.type}' for duplicate events`);
- }
- }
-
- private async handleAssetUpdate(originalEvent: EventRecord, newEvent: AssetAddEvent) {
- if (originalEvent.type !== ClientEvent.CONVERSATION.ASSET_ADD) {
- this.throwValidationError(newEvent, 'Trying to update a non-asset message as an asset message');
- }
- const newEventData = newEvent.data;
- // the preview status is not sent by the client so we fake a 'preview' status in order to cleanly handle it in the switch statement
- const ASSET_PREVIEW = 'preview';
- // similarly, no status is sent by the client when we retry sending a failed message
- const RETRY_EVENT = 'retry';
- const isPreviewEvent = !newEventData.status && !!newEventData.preview_key;
- const isRetryEvent = !!newEventData.content_length;
- const handledEvent = isRetryEvent ? RETRY_EVENT : newEventData.status;
- const previewStatus = isPreviewEvent ? ASSET_PREVIEW : handledEvent;
-
- const updateEvent = () => {
- const updatedData = {...originalEvent.data, ...newEventData};
- const updatedEvent = {...originalEvent, data: updatedData};
- return this.eventService.replaceEvent(updatedEvent);
- };
-
- switch (previewStatus) {
- case ASSET_PREVIEW:
- case RETRY_EVENT:
- case AssetTransferState.UPLOADED: {
- return updateEvent();
- }
-
- case AssetTransferState.UPLOAD_FAILED: {
- // case of both failed or canceled upload
- const fromOther = newEvent.from !== this.userState.self().id;
- const sameSender = newEvent.from === originalEvent.from;
- const selfCancel = !fromOther && newEvent.data.reason === ProtobufAsset.NotUploaded.CANCELLED;
- // we want to delete the event in the case of an error from the remote client or a cancel on the user's own client
- const shouldDeleteEvent = (fromOther || selfCancel) && sameSender;
- if (shouldDeleteEvent) {
- await this.eventService.deleteEvent(newEvent.conversation, newEvent.id);
- return newEvent;
- }
- return this.eventService.updateEventAsUploadFailed(originalEvent.primary_key, newEvent.data.reason);
- }
-
- default: {
- this.throwValidationError(newEvent, `Unhandled asset status update '${newEvent.data.status}'`);
- }
- }
- }
-
- private handleMessageUpdate(originalEvent: EventRecord, newEvent: MessageAddEvent) {
- const newEventData = newEvent.data;
- const originalData = originalEvent.data;
-
- if (originalEvent.from !== newEvent.from) {
- const logMessage = `ID previously used by user '${newEvent.from}'`;
- const errorMessage = 'ID reused by other user';
- return this.throwValidationError(newEvent, errorMessage, logMessage);
- }
-
- const containsLinkPreview = newEventData.previews && !!newEventData.previews.length;
- const isRetryAttempt = isEventRecordFailed(originalEvent) || isEventRecordWithFederationError(originalEvent);
-
- if (!containsLinkPreview && !isRetryAttempt) {
- const errorMessage =
- 'Message duplication event invalid: original message did not fail to send and does not contain link preview';
- return this.throwValidationError(newEvent, errorMessage);
- }
-
- const textContentMatches = newEventData.content === (originalData as any).content;
- if (!textContentMatches) {
- const errorMessage = 'ID of link preview reused';
- const logMessage = 'Text content for message duplication not matching';
- return this.throwValidationError(newEvent, errorMessage, logMessage);
- }
-
- const bothAreMessageAddType = newEvent.type === originalEvent.type;
- if (!bothAreMessageAddType) {
- return this.throwValidationError(newEvent, 'ID reused by same user');
- }
-
- const updates = this.getUpdatesForMessage(originalEvent, newEvent);
- const identifiedUpdates = {primary_key: originalEvent.primary_key, ...updates};
- return this.eventService.replaceEvent(identifiedUpdates);
- }
-
- private static getCommonMessageUpdates(originalEvent: StoredEvent, newEvent: MessageAddEvent) {
- return {
- ...newEvent,
- data: {...newEvent.data, expects_read_confirmation: originalEvent.data.expects_read_confirmation},
- edited_time: newEvent.time,
- read_receipts: !newEvent.read_receipts ? originalEvent.read_receipts : newEvent.read_receipts,
- status: !newEvent.status || newEvent.status < originalEvent.status ? originalEvent.status : newEvent.status,
- time: originalEvent.time,
- version: 1,
- };
- }
-
- private static getUpdatesForEditMessage(originalEvent: EventRecord, newEvent: MessageAddEvent): MessageAddEvent {
- // Remove reactions, so that likes (hearts) don't stay when a message's text gets edited
- return {...newEvent, reactions: {}};
- }
-
- private getUpdatesForMessage(originalEvent: EventRecord, newEvent: MessageAddEvent) {
- const newData = newEvent.data;
- const originalData = originalEvent.data;
- const updatingLinkPreview = !!(originalData as any).previews.length;
- if (updatingLinkPreview) {
- this.throwValidationError(newEvent, 'ID of link preview reused');
- }
-
- const textContentMatches = !newData.previews?.length || newData.content === (originalData as any).content;
- if (!textContentMatches) {
- const logMessage = 'Text content for message duplication not matching';
- const errorMessage = 'ID of duplicated message reused';
- this.throwValidationError(newEvent, errorMessage, logMessage);
- }
-
- return {
- ...newEvent,
- category: categoryFromEvent(newEvent),
- ephemeral_expires: originalEvent.ephemeral_expires,
- ephemeral_started: originalEvent.ephemeral_started,
- ephemeral_time: originalEvent.ephemeral_time,
- server_time: newEvent.time,
- time: originalEvent.time,
- version: originalEvent.version,
- };
- }
-
- private throwValidationError(event: IncomingEvent, errorMessage: string, logMessage?: string): never {
- const conversation = 'conversation' in event && event.conversation;
- const from = 'from' in event && event.from;
-
- const baseLogMessage = `Ignored '${event.type}' in '${conversation}' from '${from}''`;
- this.logger.warn(`${baseLogMessage} ${logMessage || errorMessage}`);
- throw new EventError(EventError.TYPE.VALIDATION_FAILED, `Event validation failed: ${errorMessage}`);
- }
}
diff --git a/src/script/event/EventService.ts b/src/script/event/EventService.ts
index 2e4b124669e..43fa6e0a4bb 100644
--- a/src/script/event/EventService.ts
+++ b/src/script/event/EventService.ts
@@ -39,7 +39,7 @@ import {StorageSchemata} from '../storage/StorageSchemata';
export type Includes = {includeFrom: boolean; includeTo: boolean};
type DexieCollection = Dexie.Collection;
export type DBEvents = DexieCollection | EventRecord[];
-type IdentifiedUpdatePayload = Partial & Pick;
+export type IdentifiedUpdatePayload = Partial & Pick;
export const eventTimeToDate = (time: string) => new Date(time) || new Date(parseInt(time, 10));
diff --git a/src/script/event/EventTypeHandling.ts b/src/script/event/EventTypeHandling.ts
index 70c4b8d378a..89c8ec374c2 100644
--- a/src/script/event/EventTypeHandling.ts
+++ b/src/script/event/EventTypeHandling.ts
@@ -17,10 +17,16 @@
*
*/
-import {CONVERSATION_EVENT} from '@wireapp/api-client/lib/event/';
+import {CONVERSATION_EVENT, ConversationEvent} from '@wireapp/api-client/lib/event';
import {ClientEvent} from './Client';
+import {ClientConversationEvent} from '../conversation/EventBuilder';
+
+export function eventShouldBeStored(event: {type: any}): event is ClientConversationEvent | ConversationEvent {
+ return EventTypeHandling.STORE.includes(event.type);
+}
+
export const EventTypeHandling = {
CONFIRM: [
ClientEvent.CONVERSATION.ASSET_ADD,
diff --git a/src/script/event/preprocessor/EventStorageMiddleware/EventStorageMiddleware.test.ts b/src/script/event/preprocessor/EventStorageMiddleware/EventStorageMiddleware.test.ts
new file mode 100644
index 00000000000..60ffc4fb123
--- /dev/null
+++ b/src/script/event/preprocessor/EventStorageMiddleware/EventStorageMiddleware.test.ts
@@ -0,0 +1,353 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+import {Asset as ProtobufAsset} from '@wireapp/protocol-messaging';
+
+import {AssetTransferState} from 'src/script/assets/AssetTransferState';
+import {User} from 'src/script/entity/User';
+import {EventError} from 'src/script/error/EventError';
+import {createAssetAddEvent, createMessageAddEvent, toSavedEvent} from 'test/helper/EventGenerator';
+import {createUuid} from 'Util/uuid';
+
+import {EventStorageMiddleware} from './EventStorageMiddleware';
+
+import {ClientEvent} from '../../Client';
+import {EventService} from '../../EventService';
+
+function buildEventStorageMiddleware() {
+ const eventService = {
+ saveEvent: jest.fn(event => event),
+ loadEvent: jest.fn(),
+ replaceEvent: jest.fn(event => event),
+ deleteEvent: jest.fn(),
+ } as unknown as jest.Mocked;
+
+ const selfUser = new User(createUuid());
+ return [new EventStorageMiddleware(eventService, selfUser), {eventService, selfUser}] as const;
+}
+
+describe('EventStorageMiddleware', () => {
+ describe('processEvent', () => {
+ it('ignores unhandled event', async () => {
+ const event = {type: 'other'} as any;
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+
+ await eventStorageMiddleware.processEvent(event);
+ expect(eventService.saveEvent).not.toHaveBeenCalledWith(event);
+ });
+
+ it('saves an event with a new ID', async () => {
+ const event = createMessageAddEvent();
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+
+ await eventStorageMiddleware.processEvent(event);
+ expect(eventService.saveEvent).toHaveBeenCalledWith(event);
+ });
+
+ it('fails for an event with an ID previously used by another user', async () => {
+ const event = createMessageAddEvent();
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const eventWithSameId = {...event, from: createUuid()};
+ eventService.loadEvent.mockResolvedValue({primary_key: '', category: 1, ...eventWithSameId});
+
+ await expect(eventStorageMiddleware.processEvent(event)).rejects.toEqual(
+ new EventError(
+ EventError.TYPE.VALIDATION_FAILED,
+ 'Event validation failed: ID previously used by another user',
+ ),
+ );
+ });
+
+ it('fails for a non-"text message" with an ID previously used by the same user', async () => {
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const event = createMessageAddEvent();
+ eventService.loadEvent.mockResolvedValue(toSavedEvent({...event, type: ClientEvent.CALL.E_CALL} as any));
+
+ await expect(eventStorageMiddleware.processEvent(event)).rejects.toEqual(
+ new EventError(
+ EventError.TYPE.VALIDATION_FAILED,
+ 'Event validation failed: ID already used for a different type of message',
+ ),
+ );
+ });
+
+ it('fails for a plain text message with an ID previously used by the same user', async () => {
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const event = createMessageAddEvent();
+ eventService.loadEvent.mockResolvedValue(toSavedEvent(event));
+
+ await expect(eventStorageMiddleware.processEvent(event)).rejects.toEqual(
+ new EventError(
+ EventError.TYPE.VALIDATION_FAILED,
+ 'Event validation failed: ID already used for a successfully sent message',
+ ),
+ );
+ });
+
+ it('fails for a text message with link preview with an ID previously used by the same user for a text message with link preview', async () => {
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const event = createMessageAddEvent();
+ const storedEvent = {...event, data: {...event.data, previews: ['1']}};
+ eventService.loadEvent.mockResolvedValue(toSavedEvent(storedEvent));
+
+ await expect(eventStorageMiddleware.processEvent(event)).rejects.toEqual(
+ new EventError(
+ EventError.TYPE.VALIDATION_FAILED,
+ 'Event validation failed: ID already used for a successfully sent message',
+ ),
+ );
+ });
+
+ it('ignores a text message with link preview with an ID previously used by the same user for a text message different content', async () => {
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const event = createMessageAddEvent();
+ const storedEvent = {...event, data: {...event.data, previews: [] as any[]}};
+ eventService.loadEvent.mockResolvedValue(toSavedEvent(storedEvent));
+
+ const newEvent = {...event, data: {...event.data, content: 'different content', previews: ['1']}};
+
+ await expect(eventStorageMiddleware.processEvent(newEvent)).rejects.toEqual(
+ new EventError(
+ EventError.TYPE.VALIDATION_FAILED,
+ 'Event validation failed: Link preview with different text content',
+ ),
+ );
+ });
+
+ it('saves a text message with link preview with an ID previously used by the same user for a plain text message', async () => {
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const event = createMessageAddEvent();
+ const storedEvent = JSON.parse(JSON.stringify(event));
+ eventService.loadEvent.mockResolvedValue(storedEvent);
+ eventService.replaceEvent.mockResolvedValue(storedEvent);
+
+ const initial_time = event.time;
+ const changed_time = new Date(new Date(event.time).getTime() + 60 * 1000).toISOString();
+ event.data.previews?.push('1');
+ event.time = changed_time;
+
+ const savedEvent = (await eventStorageMiddleware.processEvent(event)) as any;
+ expect(savedEvent.time).toEqual(initial_time);
+ expect(savedEvent.time).not.toEqual(changed_time);
+ expect(eventService.replaceEvent).toHaveBeenCalled();
+ });
+
+ it('saves a link preview even if the original message is not found', async () => {
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const event = createMessageAddEvent({dataOverrides: {previews: ['1']}});
+
+ await eventStorageMiddleware.processEvent(event);
+ expect(eventService.replaceEvent).not.toHaveBeenCalled();
+ expect(eventService.saveEvent).toHaveBeenCalled();
+ });
+
+ it('ignores edit message with missing associated original message', async () => {
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const linkPreviewEvent = createMessageAddEvent();
+ eventService.loadEvent.mockResolvedValue(undefined);
+
+ linkPreviewEvent.data.replacing_message_id = 'missing';
+
+ await expect(eventStorageMiddleware.processEvent(linkPreviewEvent)).rejects.toEqual(
+ new EventError(EventError.TYPE.VALIDATION_FAILED, 'Event validation failed: Edit event without original event'),
+ );
+ });
+
+ it('updates edited messages when link preview arrives', async () => {
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const replacingId = 'old-replaced-message-id';
+ const linkPreviewEvent = createMessageAddEvent();
+ const storedEvent = JSON.parse(
+ JSON.stringify({
+ ...linkPreviewEvent,
+ data: {...linkPreviewEvent.data, replacing_message_id: replacingId},
+ }),
+ );
+ eventService.loadEvent.mockResolvedValue(storedEvent);
+
+ linkPreviewEvent.data.replacing_message_id = replacingId;
+ linkPreviewEvent.data.previews = ['preview'];
+
+ const updatedEvent = (await eventStorageMiddleware.processEvent(linkPreviewEvent)) as any;
+ expect(eventService.replaceEvent).toHaveBeenCalled();
+ expect(eventService.saveEvent).not.toHaveBeenCalled();
+ expect(updatedEvent.data.previews[0]).toEqual('preview');
+ });
+
+ it('updates edited messages', async () => {
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const event = createMessageAddEvent();
+ const originalEvent = JSON.parse(JSON.stringify(event));
+ originalEvent.reactions = ['user-id'];
+ eventService.loadEvent.mockResolvedValue(originalEvent);
+
+ const initial_time = event.time;
+ const changed_time = new Date(new Date(event.time).getTime() + 60 * 1000).toISOString();
+ originalEvent.primary_key = 12;
+ event.id = createUuid();
+ event.data.content = 'new content';
+ event.data.replacing_message_id = originalEvent.id;
+ event.time = changed_time;
+
+ const updatedEvent = (await eventStorageMiddleware.processEvent(event)) as any;
+ expect(updatedEvent.time).toEqual(initial_time);
+ expect(updatedEvent.time).not.toEqual(changed_time);
+ expect(updatedEvent.data.content).toEqual('new content');
+ expect(updatedEvent.primary_key).toEqual(originalEvent.primary_key);
+ expect(Object.keys(updatedEvent.reactions).length).toEqual(0);
+ expect(eventService.replaceEvent).toHaveBeenCalled();
+ });
+
+ it('updates link preview when edited', async () => {
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const event = createMessageAddEvent();
+
+ const replacingId = 'replaced-message-id';
+ const storedEvent = {
+ ...event,
+ data: {...event.data, previews: ['preview']},
+ };
+ const editEvent = {...event};
+ eventService.loadEvent.mockResolvedValue(toSavedEvent(storedEvent));
+
+ editEvent.data.replacing_message_id = replacingId;
+
+ const updatedEvent = (await eventStorageMiddleware.processEvent(editEvent)) as any;
+ expect(eventService.replaceEvent).toHaveBeenCalled();
+ expect(eventService.saveEvent).not.toHaveBeenCalled();
+ expect(updatedEvent.data.previews.length).toEqual(0);
+ });
+
+ it('saves a conversation.asset-add event', async () => {
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const event = createAssetAddEvent();
+
+ const updatedEvent = await eventStorageMiddleware.processEvent(event);
+ expect(updatedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD);
+ expect(eventService.saveEvent).toHaveBeenCalled();
+ });
+
+ it('deletes cancelled conversation.asset-add event', async () => {
+ const [eventStorageMiddleware, {eventService, selfUser}] = buildEventStorageMiddleware();
+ const fromIds = [
+ // cancel from an other user
+ createUuid(),
+ // cancel from the self user
+ selfUser.id,
+ ];
+
+ for (const fromId of fromIds) {
+ const assetAddEvent = createAssetAddEvent({from: fromId});
+ const assetCancelEvent = {
+ ...assetAddEvent,
+ data: {
+ ...assetAddEvent.data,
+ reason: ProtobufAsset.NotUploaded.CANCELLED,
+ status: AssetTransferState.UPLOAD_FAILED,
+ },
+ time: '2017-09-06T09:43:36.528Z',
+ };
+
+ eventService.loadEvent.mockResolvedValue(toSavedEvent(assetAddEvent));
+ eventService.deleteEvent.mockResolvedValue(1);
+
+ const savedEvent = await eventStorageMiddleware.processEvent(assetCancelEvent);
+ expect(savedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD);
+ expect(eventService.deleteEvent).toHaveBeenCalled();
+ }
+ });
+
+ it('deletes other user failed upload for conversation.asset-add event', async () => {
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const assetAddEvent = createAssetAddEvent();
+ const assetUploadFailedEvent = {
+ ...assetAddEvent,
+ data: {
+ ...assetAddEvent.data,
+ reason: ProtobufAsset.NotUploaded.FAILED,
+ status: AssetTransferState.UPLOAD_FAILED,
+ },
+ time: '2017-09-06T09:43:36.528Z',
+ };
+
+ eventService.loadEvent.mockResolvedValue(toSavedEvent(assetAddEvent));
+ eventService.deleteEvent.mockResolvedValue(1);
+
+ const savedEvent = await eventStorageMiddleware.processEvent(assetUploadFailedEvent);
+ expect(savedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD);
+ expect(eventService.deleteEvent).toHaveBeenCalled();
+ });
+
+ it('updates self failed upload for conversation.asset-add event', async () => {
+ const [eventStorageMiddleware, {eventService, selfUser}] = buildEventStorageMiddleware();
+
+ const assetAddEvent = createAssetAddEvent({from: selfUser.id});
+ const assetUploadFailedEvent = {
+ ...assetAddEvent,
+ data: {
+ ...assetAddEvent.data,
+ reason: ProtobufAsset.NotUploaded.FAILED,
+ status: AssetTransferState.UPLOAD_FAILED,
+ },
+ time: '2017-09-06T09:43:36.528Z',
+ } as any;
+
+ eventService.loadEvent.mockResolvedValue(toSavedEvent(assetAddEvent));
+
+ const savedEvent = await eventStorageMiddleware.processEvent(assetUploadFailedEvent);
+ expect(savedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD);
+ expect(eventService.replaceEvent).toHaveBeenCalled();
+ });
+
+ it('handles conversation.asset-add state update event', async () => {
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const initialAssetEvent = createAssetAddEvent({type: ClientEvent.CONVERSATION.ASSET_ADD});
+
+ const updateStatusEvent = {
+ ...initialAssetEvent,
+ data: {...initialAssetEvent.data, status: AssetTransferState.UPLOADED},
+ time: '2017-09-06T09:43:36.528Z',
+ };
+
+ eventService.loadEvent.mockResolvedValue(toSavedEvent(initialAssetEvent));
+
+ const updatedEvent = (await eventStorageMiddleware.processEvent(updateStatusEvent)) as any;
+ expect(updatedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD);
+ expect(updatedEvent.data.status).toEqual(updateStatusEvent.data.status);
+ expect(eventService.replaceEvent).toHaveBeenCalled();
+ });
+
+ it('updates video when preview is received', async () => {
+ const [eventStorageMiddleware, {eventService}] = buildEventStorageMiddleware();
+ const initialAssetEvent = createAssetAddEvent();
+
+ const AssetPreviewEvent = {
+ ...initialAssetEvent,
+ data: {...initialAssetEvent.data, status: AssetTransferState.UPLOADED},
+ time: '2017-09-06T09:43:36.528Z',
+ };
+
+ eventService.loadEvent.mockResolvedValue(toSavedEvent(initialAssetEvent));
+
+ const updatedEvent = await eventStorageMiddleware.processEvent(AssetPreviewEvent);
+ expect(updatedEvent.type).toEqual(ClientEvent.CONVERSATION.ASSET_ADD);
+ expect(eventService.replaceEvent).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/script/event/preprocessor/EventStorageMiddleware/EventStorageMiddleware.ts b/src/script/event/preprocessor/EventStorageMiddleware/EventStorageMiddleware.ts
new file mode 100644
index 00000000000..66aa216cb49
--- /dev/null
+++ b/src/script/event/preprocessor/EventStorageMiddleware/EventStorageMiddleware.ts
@@ -0,0 +1,111 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+import {User} from 'src/script/entity/User';
+
+import {handleLinkPreviewEvent, handleEditEvent, handleAssetEvent} from './eventHandlers';
+import {EventValidationError} from './eventHandlers/EventValidationError';
+import {HandledEvents, DBOperation} from './types';
+
+import {isEventRecordFailed, isEventRecordWithFederationError} from '../../../message/StatusType';
+import type {EventRecord} from '../../../storage';
+import {CONVERSATION} from '../../Client';
+import {EventMiddleware, IncomingEvent} from '../../EventProcessor';
+import {EventService} from '../../EventService';
+import {eventShouldBeStored} from '../../EventTypeHandling';
+
+export class EventStorageMiddleware implements EventMiddleware {
+ constructor(
+ private readonly eventService: EventService,
+ private readonly selfUser: User,
+ ) {}
+
+ async processEvent(event: IncomingEvent) {
+ const shouldSaveEvent = eventShouldBeStored(event);
+ if (!shouldSaveEvent) {
+ return event;
+ }
+ const eventId = 'id' in event && event.id;
+ /* We try to load a potential duplicate of the event (same ID, same conversation in the DB). There are multiple valid cases for duplicates:
+ * - The event is a retry of a previously failed event
+ * - The event is a link preview of a text message previously sent
+ * - The event is an asset upload success of a metadata asset message
+ */
+ const duplicateEvent = eventId ? await this.eventService.loadEvent(event.conversation, eventId) : undefined;
+
+ // We first validate that the event is valid
+ this.validateEvent(event, duplicateEvent);
+ // Then ask the different handlers which DB operations to perform
+ const operation = await this.getDbOperation(event, duplicateEvent);
+ // And finally execute the operation
+ return operation ? this.execDBOperation(operation, event.conversation) : event;
+ }
+
+ private async getDbOperation(event: HandledEvents, duplicateEvent?: HandledEvents): Promise {
+ const handlers = [handleEditEvent, handleLinkPreviewEvent, handleAssetEvent];
+ for (const handler of handlers) {
+ const operation = await handler(event, {
+ duplicateEvent,
+ selfUserId: this.selfUser.id,
+ findEvent: eventId => this.eventService.loadEvent(event.conversation, eventId),
+ });
+ if (operation) {
+ return operation;
+ }
+ }
+ return {type: 'insert', event};
+ }
+
+ private validateEvent(event: HandledEvents, duplicateEvent?: EventRecord) {
+ if (!duplicateEvent) {
+ return;
+ }
+ if (duplicateEvent.from !== event.from) {
+ throw new EventValidationError('ID previously used by another user');
+ }
+
+ if (event.type !== duplicateEvent.type) {
+ throw new EventValidationError('ID already used for a different type of message');
+ }
+
+ if (event.type === CONVERSATION.MESSAGE_ADD && duplicateEvent.type === CONVERSATION.MESSAGE_ADD) {
+ const isValidUpdate = !!event.data.previews?.length || event.data.replacing_message_id;
+ const isRetryAttempt = isEventRecordFailed(duplicateEvent) || isEventRecordWithFederationError(duplicateEvent);
+
+ if (!isValidUpdate && !isRetryAttempt) {
+ throw new EventValidationError('ID already used for a successfully sent message');
+ }
+ }
+ }
+
+ private async execDBOperation(operation: DBOperation, conversationId: string) {
+ switch (operation.type) {
+ case 'insert':
+ return this.eventService.saveEvent(operation.event);
+
+ case 'update':
+ await this.eventService.replaceEvent(operation.updates);
+ break;
+
+ case 'delete':
+ await this.eventService.deleteEvent(conversationId, operation.id);
+ }
+ return operation.event;
+ }
+}
diff --git a/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/EventValidationError.ts b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/EventValidationError.ts
new file mode 100644
index 00000000000..68432ec025c
--- /dev/null
+++ b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/EventValidationError.ts
@@ -0,0 +1,26 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+import {EventError} from 'src/script/error/EventError';
+
+export class EventValidationError extends EventError {
+ constructor(message: string) {
+ super(EventError.TYPE.VALIDATION_FAILED, `Event validation failed: ${message}`);
+ }
+}
diff --git a/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/assetEventHandler.ts b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/assetEventHandler.ts
new file mode 100644
index 00000000000..f18489edd13
--- /dev/null
+++ b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/assetEventHandler.ts
@@ -0,0 +1,108 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+import {Asset as ProtobufAsset} from '@wireapp/protocol-messaging';
+
+import {AssetTransferState} from 'src/script/assets/AssetTransferState';
+import {AssetAddEvent} from 'src/script/conversation/EventBuilder';
+import {StoredEvent} from 'src/script/storage';
+
+import {EventValidationError} from './EventValidationError';
+
+import {CONVERSATION, ClientEvent} from '../../../Client';
+import {DBOperation, EventHandler, HandledEvents} from '../types';
+
+function validateAssetEvent(originalEvent: HandledEvents | undefined): originalEvent is StoredEvent {
+ if (!originalEvent) {
+ return false;
+ }
+
+ if (originalEvent.type !== ClientEvent.CONVERSATION.ASSET_ADD) {
+ throw new EventValidationError('Trying to update a non-asset message as an asset message');
+ }
+
+ return true;
+}
+
+function computeEventUpdates(
+ originalEvent: StoredEvent,
+ newEvent: AssetAddEvent,
+ selfUserId: string,
+): DBOperation {
+ const newEventData = newEvent.data;
+ // the preview status is not sent by the client so we fake a 'preview' status in order to cleanly handle it in the switch statement
+ const ASSET_PREVIEW = 'preview';
+ // similarly, no status is sent by the client when we retry sending a failed message
+ const RETRY_EVENT = 'retry';
+ const isPreviewEvent = !newEventData.status && !!newEventData.preview_key;
+ const isRetryEvent = !!newEventData.content_length;
+ const handledEvent = isRetryEvent ? RETRY_EVENT : newEventData.status;
+ const previewStatus = isPreviewEvent ? ASSET_PREVIEW : handledEvent;
+
+ const updateEventData = (newData: Partial) => {
+ return {
+ ...originalEvent,
+ data: {...originalEvent.data, ...newData},
+ };
+ };
+
+ switch (previewStatus) {
+ case ASSET_PREVIEW:
+ case RETRY_EVENT:
+ case AssetTransferState.UPLOADED: {
+ return {type: 'update', event: newEvent, updates: updateEventData(newEventData)};
+ }
+
+ case AssetTransferState.UPLOAD_FAILED: {
+ // case of both failed or canceled upload
+ const fromOther = newEvent.from !== selfUserId;
+ const sameSender = newEvent.from === originalEvent.from;
+ const selfCancel = !fromOther && newEvent.data.reason === ProtobufAsset.NotUploaded.CANCELLED;
+ // we want to delete the event in the case of an error from the remote client or a cancel on the user's own client
+ const shouldDeleteEvent = (fromOther || selfCancel) && sameSender;
+ if (shouldDeleteEvent) {
+ return {type: 'delete', event: newEvent, id: newEvent.id};
+ }
+
+ const updatedEvent = updateEventData({
+ status: AssetTransferState.UPLOAD_FAILED,
+ reason: newEvent.data.reason ?? ProtobufAsset.NotUploaded.FAILED,
+ });
+ return {
+ type: 'update',
+ event: updatedEvent,
+ updates: updatedEvent,
+ };
+ }
+
+ default: {
+ throw new EventValidationError(`Unhandled asset status update '${newEvent.data.status}'`);
+ }
+ }
+}
+
+export const handleAssetEvent: EventHandler = async (event, {duplicateEvent, selfUserId}) => {
+ if (event.type !== CONVERSATION.ASSET_ADD) {
+ return undefined;
+ }
+ if (validateAssetEvent(duplicateEvent)) {
+ return computeEventUpdates(duplicateEvent, event, selfUserId);
+ }
+ return undefined;
+};
diff --git a/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/editedEventHandler.ts b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/editedEventHandler.ts
new file mode 100644
index 00000000000..80f5f88065f
--- /dev/null
+++ b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/editedEventHandler.ts
@@ -0,0 +1,83 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+import {MessageAddEvent} from 'src/script/conversation/EventBuilder';
+import {EventError} from 'src/script/error/EventError';
+import {StoredEvent} from 'src/script/storage';
+
+import {getCommonMessageUpdates} from './getCommonMessageUpdates';
+
+import {CONVERSATION, ClientEvent} from '../../../Client';
+import {EventHandler, HandledEvents} from '../types';
+
+function throwValidationError(message: string): never {
+ throw new EventError(EventError.TYPE.VALIDATION_FAILED, `Event validation failed: ${message}`);
+}
+
+function validateEditEvent(
+ originalEvent: HandledEvents | undefined,
+ editEvent: MessageAddEvent,
+): originalEvent is StoredEvent {
+ if (!originalEvent) {
+ throwValidationError('Edit event without original event');
+ }
+
+ if (originalEvent.type !== ClientEvent.CONVERSATION.MESSAGE_ADD) {
+ throwValidationError('Edit event for non-text message');
+ }
+
+ if (originalEvent.from !== editEvent.from) {
+ throwValidationError('ID reused by other user');
+ }
+
+ return true;
+}
+
+function getUpdatesForEditMessage(
+ originalEvent: StoredEvent,
+ newEvent: MessageAddEvent,
+): MessageAddEvent {
+ // Remove reactions, so that likes (hearts) don't stay when a message's text gets edited
+ const commonUpdates = getCommonMessageUpdates(originalEvent, newEvent);
+
+ return {...newEvent, ...commonUpdates, reactions: {}};
+}
+
+function computeEventUpdates(originalEvent: StoredEvent, newEvent: MessageAddEvent) {
+ const primaryKeyUpdate = {primary_key: originalEvent.primary_key};
+ const updates = getUpdatesForEditMessage(originalEvent, newEvent);
+
+ return {...primaryKeyUpdate, ...updates};
+}
+
+export const handleEditEvent: EventHandler = async (event, {findEvent}) => {
+ if (event.type !== CONVERSATION.MESSAGE_ADD) {
+ return undefined;
+ }
+ const editedEventId = event.data.replacing_message_id;
+ if (!editedEventId) {
+ return undefined;
+ }
+ const originalEvent = await findEvent(editedEventId);
+ if (validateEditEvent(originalEvent, event)) {
+ const updatedEvent = computeEventUpdates(originalEvent, event);
+ return {type: 'update', event: updatedEvent, updates: updatedEvent};
+ }
+ return undefined;
+};
diff --git a/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/getCommonMessageUpdates.test.ts b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/getCommonMessageUpdates.test.ts
new file mode 100644
index 00000000000..ffe78482f6c
--- /dev/null
+++ b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/getCommonMessageUpdates.test.ts
@@ -0,0 +1,48 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+import {StatusType} from 'src/script/message/StatusType';
+import {createMessageAddEvent, toSavedEvent} from 'test/helper/EventGenerator';
+
+import {getCommonMessageUpdates} from './getCommonMessageUpdates';
+
+describe('getCommonMessageUpdates', () => {
+ /** @see https://wearezeta.atlassian.net/browse/SQCORE-732 */
+ it('does not overwrite the seen status if a message gets edited', () => {
+ const originalEvent = toSavedEvent(createMessageAddEvent({overrides: {status: StatusType.SEEN}}));
+
+ const editedEvent = createMessageAddEvent({
+ text: 'Edited Text Which Replaces The Original Text',
+ overrides: {
+ read_receipts: [
+ {
+ time: '2021-06-10T19:47:19.570Z',
+ userId: 'b661e27f-24c6-4c52-a425-87a7b7f3df61',
+ },
+ ],
+ },
+ dataOverrides: {replacing_message_id: originalEvent.id},
+ });
+
+ const updatedEvent = getCommonMessageUpdates(originalEvent, editedEvent) as any;
+ expect(updatedEvent.data.content).toBe('Edited Text Which Replaces The Original Text');
+ expect(updatedEvent.status).toBe(StatusType.SEEN);
+ expect(Object.keys(updatedEvent.read_receipts).length).toBe(1);
+ });
+});
diff --git a/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/getCommonMessageUpdates.ts b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/getCommonMessageUpdates.ts
new file mode 100644
index 00000000000..776a890cefc
--- /dev/null
+++ b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/getCommonMessageUpdates.ts
@@ -0,0 +1,32 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+import {MessageAddEvent} from 'src/script/conversation/EventBuilder';
+import {StoredEvent} from 'src/script/storage';
+
+export function getCommonMessageUpdates(originalEvent: StoredEvent, newEvent: MessageAddEvent) {
+ return {
+ ...newEvent,
+ data: {...newEvent.data, expects_read_confirmation: originalEvent.data.expects_read_confirmation},
+ edited_time: newEvent.time,
+ read_receipts: !newEvent.read_receipts ? originalEvent.read_receipts : newEvent.read_receipts,
+ status: !newEvent.status || newEvent.status < originalEvent.status ? originalEvent.status : newEvent.status,
+ time: originalEvent.time,
+ };
+}
diff --git a/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/index.ts b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/index.ts
new file mode 100644
index 00000000000..469bf765b44
--- /dev/null
+++ b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+export * from './editedEventHandler';
+export * from './linkPreviewEventHandler';
+export * from './assetEventHandler';
diff --git a/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/linkPreviewEventHandler.ts b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/linkPreviewEventHandler.ts
new file mode 100644
index 00000000000..040aa14e07a
--- /dev/null
+++ b/src/script/event/preprocessor/EventStorageMiddleware/eventHandlers/linkPreviewEventHandler.ts
@@ -0,0 +1,88 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+import {MessageAddEvent} from 'src/script/conversation/EventBuilder';
+import {categoryFromEvent} from 'src/script/message/MessageCategorization';
+import {StoredEvent} from 'src/script/storage';
+
+import {EventValidationError} from './EventValidationError';
+import {getCommonMessageUpdates} from './getCommonMessageUpdates';
+
+import {CONVERSATION, ClientEvent} from '../../../Client';
+import {EventHandler, HandledEvents} from '../types';
+
+function getLinkPreviewUpdates(originalEvent: StoredEvent, newEvent: MessageAddEvent) {
+ const commonUpdates = getCommonMessageUpdates(originalEvent, newEvent);
+
+ return {
+ ...newEvent,
+ ...commonUpdates,
+ category: categoryFromEvent(newEvent),
+ ephemeral_expires: originalEvent.ephemeral_expires,
+ ephemeral_started: originalEvent.ephemeral_started,
+ ephemeral_time: originalEvent.ephemeral_time,
+ server_time: newEvent.time,
+ version: originalEvent.version,
+ };
+}
+
+function validateLinkPreviewEvent(
+ originalEvent: HandledEvents | undefined,
+ editEvent: MessageAddEvent,
+): originalEvent is StoredEvent {
+ const {previews, content} = editEvent.data;
+ if (!previews?.length) {
+ return false;
+ }
+ if (!originalEvent) {
+ // It is fine to receive a linkPreview message without the original event
+ return false;
+ }
+ if (originalEvent.type !== ClientEvent.CONVERSATION.MESSAGE_ADD) {
+ throw new EventValidationError('Link preview event for non-text message');
+ }
+
+ const {previews: originalPreviews, content: originalContent} = originalEvent.data;
+ if (!!originalPreviews?.length) {
+ throw new EventValidationError('Link preview already existing on original message');
+ }
+
+ if (content !== originalContent) {
+ throw new EventValidationError('Link preview with different text content');
+ }
+ return true;
+}
+
+function computeEventUpdates(originalEvent: StoredEvent, newEvent: MessageAddEvent) {
+ const primaryKeyUpdate = {primary_key: originalEvent.primary_key};
+ const updates = getLinkPreviewUpdates(originalEvent, newEvent);
+
+ return {...primaryKeyUpdate, ...updates};
+}
+
+export const handleLinkPreviewEvent: EventHandler = async (event, {duplicateEvent}) => {
+ if (event.type !== CONVERSATION.MESSAGE_ADD) {
+ return undefined;
+ }
+ if (validateLinkPreviewEvent(duplicateEvent, event)) {
+ const updatedEvent = computeEventUpdates(duplicateEvent, event);
+ return {type: 'update', event: updatedEvent, updates: updatedEvent};
+ }
+ return undefined;
+};
diff --git a/src/script/event/preprocessor/EventStorageMiddleware/index.ts b/src/script/event/preprocessor/EventStorageMiddleware/index.ts
new file mode 100644
index 00000000000..0154fc9a98c
--- /dev/null
+++ b/src/script/event/preprocessor/EventStorageMiddleware/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+export * from './EventStorageMiddleware';
diff --git a/src/script/event/preprocessor/EventStorageMiddleware/types.ts b/src/script/event/preprocessor/EventStorageMiddleware/types.ts
new file mode 100644
index 00000000000..66a79d03ef1
--- /dev/null
+++ b/src/script/event/preprocessor/EventStorageMiddleware/types.ts
@@ -0,0 +1,40 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ *
+ */
+
+import {ConversationEvent} from '@wireapp/api-client/lib/event';
+
+import {EventRecord} from 'src/script/storage';
+
+import {ClientConversationEvent} from '../../../conversation/EventBuilder';
+import {IdentifiedUpdatePayload} from '../../EventService';
+
+export type HandledEvents = ClientConversationEvent | ConversationEvent;
+export type DBOperation =
+ | {type: 'update'; event: HandledEvents; updates: IdentifiedUpdatePayload}
+ | {type: 'delete'; event: HandledEvents; id: string}
+ | {type: 'insert'; event: HandledEvents};
+
+export type EventHandler = (
+ event: HandledEvents,
+ optionals: {
+ selfUserId: string;
+ duplicateEvent: HandledEvents | undefined;
+ findEvent: (eventId: string) => Promise;
+ },
+) => Promise;
diff --git a/src/script/event/preprocessor/QuotedMessageMiddleware.ts b/src/script/event/preprocessor/QuotedMessageMiddleware.ts
index 8c1652b5d8b..73f43f39573 100644
--- a/src/script/event/preprocessor/QuotedMessageMiddleware.ts
+++ b/src/script/event/preprocessor/QuotedMessageMiddleware.ts
@@ -64,9 +64,9 @@ export class QuotedMessageMiddleware implements EventMiddleware {
const originalMessageId = event.data.message_id;
const {replies} = await this.findRepliesToMessage(event.conversation, originalMessageId);
this.logger.info(`Invalidating '${replies.length}' replies to deleted message '${originalMessageId}'`);
- replies.forEach(reply => {
+ replies.forEach(async reply => {
reply.data.quote = {error: {type: QuoteEntity.ERROR.MESSAGE_NOT_FOUND}};
- this.eventService.replaceEvent(reply);
+ await this.eventService.replaceEvent(reply);
});
return event;
}
@@ -78,13 +78,12 @@ export class QuotedMessageMiddleware implements EventMiddleware {
}
this.logger.info(`Updating '${replies.length}' replies to updated message '${originalMessageId}'`);
- replies.forEach(reply => {
+ replies.forEach(async reply => {
const quote = reply.data.quote;
if (quote && typeof quote !== 'string' && 'message_id' in quote && 'id' in event) {
quote.message_id = event.id as string;
}
- // we want to update the messages quoting the original message later, thus the timeout
- setTimeout(() => this.eventService.replaceEvent(reply));
+ await this.eventService.replaceEvent(reply);
});
const decoratedData = {...event.data, quote: originalEvent.data.quote};
return {...event, data: decoratedData};
diff --git a/src/script/main/app.ts b/src/script/main/app.ts
index 966b856490b..c2ee80b73e4 100644
--- a/src/script/main/app.ts
+++ b/src/script/main/app.ts
@@ -70,6 +70,7 @@ import {EventRepository} from '../event/EventRepository';
import {EventService} from '../event/EventService';
import {EventServiceNoCompound} from '../event/EventServiceNoCompound';
import {NotificationService} from '../event/NotificationService';
+import {EventStorageMiddleware} from '../event/preprocessor/EventStorageMiddleware';
import {QuotedMessageMiddleware} from '../event/preprocessor/QuotedMessageMiddleware';
import {ReceiptsMiddleware} from '../event/preprocessor/ReceiptsMiddleware';
import {ServiceMiddleware} from '../event/preprocessor/ServiceMiddleware';
@@ -380,11 +381,17 @@ export class App {
await initializeDataDog(this.config, selfUser.qualifiedId);
// Setup all event middleware
+ const eventStorageMiddleware = new EventStorageMiddleware(this.service.event, selfUser);
const serviceMiddleware = new ServiceMiddleware(conversationRepository, userRepository, selfUser);
const quotedMessageMiddleware = new QuotedMessageMiddleware(this.service.event);
const readReceiptMiddleware = new ReceiptsMiddleware(this.service.event, conversationRepository, selfUser);
- eventRepository.setEventProcessMiddlewares([serviceMiddleware, quotedMessageMiddleware, readReceiptMiddleware]);
+ eventRepository.setEventProcessMiddlewares([
+ serviceMiddleware,
+ readReceiptMiddleware,
+ eventStorageMiddleware,
+ quotedMessageMiddleware,
+ ]);
// Setup all the event processors
const federationEventProcessor = new FederationEventProcessor(eventRepository, serverTimeHandler, selfUser);
eventRepository.setEventProcessors([federationEventProcessor]);
@@ -422,15 +429,16 @@ export class App {
await conversationRepository.conversationRoleRepository.loadTeamRoles();
+ let totalNotifications = 0;
await eventRepository.connectWebSocket(this.core, ({done, total}) => {
const baseMessage = t('initDecryption');
const extraInfo = this.config.FEATURE.SHOW_LOADING_INFORMATION
? ` ${t('initProgress', {number1: done.toString(), number2: total.toString()})}`
: '';
+ totalNotifications = total;
onProgress(25 + 50 * (done / total), `${baseMessage}${extraInfo}`);
});
- const notificationsCount = eventRepository.notificationsTotal;
await conversationRepository.init1To1Conversations(connections, conversations);
@@ -445,7 +453,7 @@ export class App {
}
telemetry.timeStep(AppInitTimingsStep.UPDATED_FROM_NOTIFICATIONS);
- telemetry.addStatistic(AppInitStatisticsValue.NOTIFICATIONS, notificationsCount, 100);
+ telemetry.addStatistic(AppInitStatisticsValue.NOTIFICATIONS, totalNotifications, 100);
eventTrackerRepository.init(propertiesRepository.properties.settings.privacy.telemetry_sharing);
onProgress(97.5, t('initUpdatedFromNotifications', this.config.BRAND_NAME));
diff --git a/src/script/page/LeftSidebar/panels/Archive.tsx b/src/script/page/LeftSidebar/panels/Archive.tsx
index 683cc07b84c..dc530fa3904 100644
--- a/src/script/page/LeftSidebar/panels/Archive.tsx
+++ b/src/script/page/LeftSidebar/panels/Archive.tsx
@@ -56,8 +56,6 @@ const Archive = ({
]);
const onClickConversation = async (conversation: Conversation) => {
- await conversationRepository.unarchiveConversation(conversation, true, 'opened conversation from archive');
- onClose();
amplify.publish(WebAppEvents.CONVERSATION.SHOW, conversation, {});
};
@@ -68,6 +66,7 @@ const Archive = ({
const {currentFocus, handleKeyDown, resetConversationFocus} = useConversationFocus(conversations);
+ const isActiveConversation = (conversation: Conversation) => conversationState.isActiveConversation(conversation);
return (
{t('archiveHeader')}
@@ -85,6 +84,7 @@ const Archive = ({
conversation={conversation}
onJoinCall={answerCall}
showJoinButton={false}
+ isSelected={isActiveConversation}
/>
))}
diff --git a/src/script/util/test/mock/WebRTCMock.ts b/src/script/util/test/mock/WebRTCMock.ts
index 880fa5cd218..c5338e82a16 100644
--- a/src/script/util/test/mock/WebRTCMock.ts
+++ b/src/script/util/test/mock/WebRTCMock.ts
@@ -19,14 +19,18 @@
import wrtc from '@koush/wrtc';
-const {RTCAudioSource} = wrtc.nonstandard;
+const {RTCAudioSource, RTCRtpSender} = wrtc.nonstandard;
declare global {
interface Window {
MediaStream: typeof wrtc.MediaStream;
RTCAudioSource: typeof RTCAudioSource;
+ RTCRtpSender: typeof RTCRtpSender;
}
}
+const RTCRtpSenderMock: Window['RTCRtpSender'] = {
+ prototype: {createEncodedVideoStreams: {}, createEncodedStreams: {}, transform: {}},
+};
Object.defineProperty(window, 'MediaStream', {
value: wrtc.MediaStream,
@@ -37,3 +41,8 @@ Object.defineProperty(window, 'RTCAudioSource', {
value: RTCAudioSource,
writable: true,
});
+
+Object.defineProperty(window, 'RTCRtpSender', {
+ value: RTCRtpSenderMock,
+ writable: true,
+});
diff --git a/src/script/view_model/ActionsViewModel.ts b/src/script/view_model/ActionsViewModel.ts
index 1296c1da753..93e08c11564 100644
--- a/src/script/view_model/ActionsViewModel.ts
+++ b/src/script/view_model/ActionsViewModel.ts
@@ -352,10 +352,6 @@ export class ActionsViewModel {
};
private readonly openConversation = async (conversationEntity: Conversation): Promise => {
- if (conversationEntity.is_archived()) {
- await this.conversationRepository.unarchiveConversation(conversationEntity, true);
- }
-
if (conversationEntity.is_cleared()) {
conversationEntity.cleared_timestamp(0);
}
diff --git a/src/script/view_model/ContentViewModel.ts b/src/script/view_model/ContentViewModel.ts
index 6a052d29ec5..023993473bb 100644
--- a/src/script/view_model/ContentViewModel.ts
+++ b/src/script/view_model/ContentViewModel.ts
@@ -133,12 +133,12 @@ export class ContentViewModel {
}
}
- changeConversation = (conversationEntity: Conversation, messageEntity?: Message) => {
+ private changeConversation(conversationEntity: Conversation, messageEntity?: Message): void {
this.initialMessage = messageEntity;
this.conversationState.activeConversation(conversationEntity);
- };
+ }
- private readonly getConversationToDisplay = async (
+ private readonly getConversationEntity = async (
conversation: Conversation | string,
domain: string | null = null,
): Promise => {
@@ -153,6 +153,76 @@ export class ContentViewModel {
return this.conversationRepository.init1to1Conversation(conversationEntity, true);
};
+ private closeRightSidebar(): void {
+ const {rightSidebar} = useAppMainState.getState();
+ rightSidebar.close();
+ }
+
+ private handleMissingConversation(): void {
+ this.closeRightSidebar();
+ return this.switchContent(ContentState.CONNECTION_REQUESTS);
+ }
+
+ private isConversationOpen(conversationEntity: Conversation, isActiveConversation: boolean): boolean {
+ const {contentState} = useAppState.getState();
+ const isConversationState = contentState === ContentState.CONVERSATION;
+ return conversationEntity && isActiveConversation && isConversationState;
+ }
+
+ private switchToNotificationSettingsIfApplicable(
+ openNotificationSettings: boolean,
+ conversationEntity: Conversation,
+ ): void {
+ if (openNotificationSettings) {
+ const {rightSidebar} = useAppMainState.getState();
+ rightSidebar.goTo(PanelState.NOTIFICATIONS, {entity: conversationEntity});
+ }
+ }
+
+ private handleConversationState(
+ isOpenedConversation: boolean,
+ openNotificationSettings: boolean,
+ conversationEntity: Conversation,
+ ): void {
+ const {setContentState} = useAppState.getState();
+ if (isOpenedConversation) {
+ this.switchToNotificationSettingsIfApplicable(openNotificationSettings, conversationEntity);
+ return;
+ }
+ setContentState(ContentState.CONVERSATION);
+
+ this.mainViewModel.list.openConversations(conversationEntity.archivedState());
+ }
+ private showAndNavigate(conversationEntity: Conversation, openNotificationSettings: boolean): void {
+ const {rightSidebar} = useAppMainState.getState();
+ this.showContent(ContentState.CONVERSATION);
+ this.previousConversation = this.conversationState.activeConversation();
+ setHistoryParam(
+ generateConversationUrl({id: conversationEntity?.id ?? '', domain: conversationEntity?.domain ?? ''}),
+ history.state,
+ );
+ if (openNotificationSettings) {
+ rightSidebar.goTo(PanelState.NOTIFICATIONS, {entity: this.conversationState.activeConversation() ?? null});
+ }
+ }
+
+ private showConversationNotFoundErrorModal(): void {
+ PrimaryModal.show(
+ PrimaryModal.type.ACKNOWLEDGE,
+ {
+ text: {
+ message: t('conversationNotFoundMessage'),
+ title: t('conversationNotFoundTitle', Config.getConfig().BRAND_NAME),
+ },
+ },
+ undefined,
+ );
+ }
+
+ private isConversationNotFoundError(error: any): boolean {
+ return error.type === ConversationError.TYPE.CONVERSATION_NOT_FOUND;
+ }
+
/**
* Opens the specified conversation.
*
@@ -173,20 +243,15 @@ export class ContentViewModel {
openNotificationSettings = false,
} = options;
- const {rightSidebar} = useAppMainState.getState();
- const {contentState, setContentState} = useAppState.getState();
-
if (!conversation) {
- rightSidebar.close();
- return this.switchContent(ContentState.CONNECTION_REQUESTS);
+ return this.handleMissingConversation();
}
try {
- const conversationEntity = await this.getConversationToDisplay(conversation, domain);
+ const conversationEntity = await this.getConversationEntity(conversation, domain);
if (!conversationEntity) {
- rightSidebar.close();
-
+ this.closeRightSidebar();
throw new ConversationError(
ConversationError.TYPE.CONVERSATION_NOT_FOUND,
ConversationError.MESSAGE.CONVERSATION_NOT_FOUND,
@@ -196,60 +261,25 @@ export class ContentViewModel {
const isActiveConversation = this.conversationState.isActiveConversation(conversationEntity);
if (!isActiveConversation) {
- rightSidebar.close();
+ this.closeRightSidebar();
}
- const isConversationState = contentState === ContentState.CONVERSATION;
- const isOpenedConversation = conversationEntity && isActiveConversation && isConversationState;
-
- if (isOpenedConversation) {
- if (openNotificationSettings) {
- rightSidebar.goTo(PanelState.NOTIFICATIONS, {entity: conversationEntity});
- }
- return;
- }
-
- setContentState(ContentState.CONVERSATION);
- this.mainViewModel.list.openConversations();
+ const isOpenedConversation = this.isConversationOpen(conversationEntity, isActiveConversation);
+ this.handleConversationState(isOpenedConversation, openNotificationSettings, conversationEntity);
if (!isActiveConversation) {
this.conversationState.activeConversation(conversationEntity);
}
- const messageEntity = openFirstSelfMention ? conversationEntity.getFirstUnreadSelfMention() : exposeMessageEntity;
-
if (conversationEntity.is_cleared()) {
conversationEntity.cleared_timestamp(0);
}
-
- if (conversationEntity.is_archived()) {
- await this.conversationRepository.unarchiveConversation(conversationEntity);
- }
-
+ const messageEntity = openFirstSelfMention ? conversationEntity.getFirstUnreadSelfMention() : exposeMessageEntity;
this.changeConversation(conversationEntity, messageEntity);
- this.showContent(ContentState.CONVERSATION);
- this.previousConversation = this.conversationState.activeConversation();
- setHistoryParam(
- generateConversationUrl({id: conversationEntity?.id ?? '', domain: conversationEntity?.domain ?? ''}),
- history.state,
- );
-
- if (openNotificationSettings) {
- rightSidebar.goTo(PanelState.NOTIFICATIONS, {entity: this.conversationState.activeConversation() ?? null});
- }
- } catch (error) {
- const isConversationNotFound = error.type === ConversationError.TYPE.CONVERSATION_NOT_FOUND;
- if (isConversationNotFound) {
- PrimaryModal.show(
- PrimaryModal.type.ACKNOWLEDGE,
- {
- text: {
- message: t('conversationNotFoundMessage'),
- title: t('conversationNotFoundTitle', Config.getConfig().BRAND_NAME),
- },
- },
- undefined,
- );
+ this.showAndNavigate(conversationEntity, openNotificationSettings);
+ } catch (error: any) {
+ if (this.isConversationNotFoundError(error)) {
+ this.showConversationNotFoundErrorModal();
} else {
throw error;
}
diff --git a/src/script/view_model/ListViewModel.ts b/src/script/view_model/ListViewModel.ts
index 038f7641a64..0697aea9dfe 100644
--- a/src/script/view_model/ListViewModel.ts
+++ b/src/script/view_model/ListViewModel.ts
@@ -132,7 +132,6 @@ export class ListViewModel {
}
private readonly _initSubscriptions = () => {
- amplify.subscribe(WebAppEvents.CONVERSATION.SHOW, this.openConversations);
amplify.subscribe(WebAppEvents.PREFERENCES.MANAGE_ACCOUNT, this.openPreferencesAccount);
amplify.subscribe(WebAppEvents.PREFERENCES.MANAGE_DEVICES, this.openPreferencesDevices);
amplify.subscribe(WebAppEvents.PREFERENCES.SHOW_AV, this.openPreferencesAudioVideo);
@@ -279,8 +278,12 @@ export class ListViewModel {
}
};
- readonly openConversations = (): void => {
- const newState = this.isActivatedAccount() ? ListState.CONVERSATIONS : ListState.TEMPORARY_GUEST;
+ readonly openConversations = (archive = false): void => {
+ const newState = this.isActivatedAccount()
+ ? archive
+ ? ListState.ARCHIVE
+ : ListState.CONVERSATIONS
+ : ListState.TEMPORARY_GUEST;
this.switchList(newState, false);
};
diff --git a/src/types/NodeModules.d.ts b/src/types/NodeModules.d.ts
index c2cc54f71fc..1ca9d3d6621 100644
--- a/src/types/NodeModules.d.ts
+++ b/src/types/NodeModules.d.ts
@@ -20,6 +20,7 @@
declare module '@koush/wrtc' {
export const nonstandard: {
RTCAudioSource: any;
+ RTCRtpSender: {prototype: {createEncodedVideoStreams: any; createEncodedStreams: any; transform: any}};
};
export const MediaStream: any;
}
diff --git a/test/helper/EventGenerator.ts b/test/helper/EventGenerator.ts
index 6593f7cb016..b27bfab3f30 100644
--- a/test/helper/EventGenerator.ts
+++ b/test/helper/EventGenerator.ts
@@ -87,7 +87,9 @@ export function createAssetAddEvent(overrides: Partial = {}): Ass
* @param event
* @returns
*/
-export function toSavedEvent(event: MessageAddEvent | AssetAddEvent) {
+export function toSavedEvent(
+ event: T,
+): T & {primary_key: string; category: number} {
return {
primary_key: createUuid(),
category: 1,
diff --git a/test/helper/UserGenerator.ts b/test/helper/UserGenerator.ts
index 32a44806e95..71441fc9453 100644
--- a/test/helper/UserGenerator.ts
+++ b/test/helper/UserGenerator.ts
@@ -48,7 +48,7 @@ export function generateAPIUser(
handle: faker.internet.userName(),
id: id.id,
// replace special chars to avoid escaping problems with querying the DOM
- name: faker.person.fullName().replace(/[^a-zA-Z ]/, ''),
+ name: faker.person.fullName().replace(/[^a-zA-Z ]/g, ''),
qualified_id: id,
...overwites,
};
diff --git a/yarn.lock b/yarn.lock
index 5b5f3c9a3df..77abeed1cbd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2578,6 +2578,13 @@ __metadata:
languageName: node
linkType: hard
+"@eslint/js@npm:8.52.0":
+ version: 8.52.0
+ resolution: "@eslint/js@npm:8.52.0"
+ checksum: 490893b8091a66415f4ac98b963d23eb287264ea3bd6af7ec788f0570705cf64fd6ab84b717785980f55e39d08ff5c7fde6d8e4391ccb507169370ce3a6d091a
+ languageName: node
+ linkType: hard
+
"@faker-js/faker@npm:8.1.0":
version: 8.1.0
resolution: "@faker-js/faker@npm:8.1.0"
@@ -2611,9 +2618,9 @@ __metadata:
languageName: node
linkType: hard
-"@formatjs/cli@npm:6.2.0":
- version: 6.2.0
- resolution: "@formatjs/cli@npm:6.2.0"
+"@formatjs/cli@npm:6.2.1":
+ version: 6.2.1
+ resolution: "@formatjs/cli@npm:6.2.1"
peerDependencies:
vue: ^3.3.4
peerDependenciesMeta:
@@ -2621,7 +2628,7 @@ __metadata:
optional: true
bin:
formatjs: bin/formatjs
- checksum: 4afc9535b32d14622e02264f527276117fec9ead67c8172d7f5fa66199b180dc7de7c1a180e36d95e31a1bd366fe826cbfdf510f5b3e86ae2750f01abbd386f1
+ checksum: 60404e080f21eb87972007524a3c79ffb5c76509986247e5351bcb5e3fc3c7f88dbbd7bdb8e2986d26734659bc06ad0a5b0498e4c0928ea3e74da30eeabc060b
languageName: node
linkType: hard
@@ -2644,14 +2651,14 @@ __metadata:
languageName: node
linkType: hard
-"@formatjs/icu-messageformat-parser@npm:2.6.2":
- version: 2.6.2
- resolution: "@formatjs/icu-messageformat-parser@npm:2.6.2"
+"@formatjs/icu-messageformat-parser@npm:2.7.0":
+ version: 2.7.0
+ resolution: "@formatjs/icu-messageformat-parser@npm:2.7.0"
dependencies:
"@formatjs/ecma402-abstract": 1.17.2
"@formatjs/icu-skeleton-parser": 1.6.2
tslib: ^2.4.0
- checksum: c339a712497deaa14e84610afb6c09377a5641b18fb0ae3d42999354cf2b557c84d7c7fead0d06d38a04f8e63e8002ae51c820a8a3e98b82805e84e7878e9d90
+ checksum: 5c289b0090a3549fdeb5ee4c382666cf085be77c8a10abcee879e12e064126ec803a48d7f564a3cb5baf4529ef5fde3a61b30f9c54481279f0fb2cf2e9be1e7e
languageName: node
linkType: hard
@@ -2665,25 +2672,25 @@ __metadata:
languageName: node
linkType: hard
-"@formatjs/intl-displaynames@npm:6.5.2":
- version: 6.5.2
- resolution: "@formatjs/intl-displaynames@npm:6.5.2"
+"@formatjs/intl-displaynames@npm:6.6.1":
+ version: 6.6.1
+ resolution: "@formatjs/intl-displaynames@npm:6.6.1"
dependencies:
"@formatjs/ecma402-abstract": 1.17.2
"@formatjs/intl-localematcher": 0.4.2
tslib: ^2.4.0
- checksum: e48718c00fc2392aac49bbc6971f44d6e56f3626aaebae9ecf61fe8787b7e11b5b0be9f7be5d8bcd43ea6fbaa7ea4aa1453b042e076f982fa0d24d7744088ccf
+ checksum: e70c18f5f6228fbf937434c9168eac4d42c7f115410b42b66785ba43db4604184849d9e55b76ca4ce116359fec645b5e41e06b036dcb0966387c19753970c8e5
languageName: node
linkType: hard
-"@formatjs/intl-listformat@npm:7.4.2":
- version: 7.4.2
- resolution: "@formatjs/intl-listformat@npm:7.4.2"
+"@formatjs/intl-listformat@npm:7.5.0":
+ version: 7.5.0
+ resolution: "@formatjs/intl-listformat@npm:7.5.0"
dependencies:
"@formatjs/ecma402-abstract": 1.17.2
"@formatjs/intl-localematcher": 0.4.2
tslib: ^2.4.0
- checksum: e31c0f8cf91c23c22c725db6fee51f5a54399a04d7dc1834a7a83e2d7069e26b92f7276a10c4f35d4ffa82f482c1fcf1240571f2e04e10ef072f2edcd2f69a09
+ checksum: 55de558bc7981a0ad442dada2500f369ad05176e7a997cf6ac502b8d446b2ae339477360219553c05d3fce7526823620866d1ba06d4006a00f4712aead5335ab
languageName: node
linkType: hard
@@ -2696,23 +2703,23 @@ __metadata:
languageName: node
linkType: hard
-"@formatjs/intl@npm:2.9.3":
- version: 2.9.3
- resolution: "@formatjs/intl@npm:2.9.3"
+"@formatjs/intl@npm:2.9.5":
+ version: 2.9.5
+ resolution: "@formatjs/intl@npm:2.9.5"
dependencies:
"@formatjs/ecma402-abstract": 1.17.2
"@formatjs/fast-memoize": 2.2.0
- "@formatjs/icu-messageformat-parser": 2.6.2
- "@formatjs/intl-displaynames": 6.5.2
- "@formatjs/intl-listformat": 7.4.2
- intl-messageformat: 10.5.3
+ "@formatjs/icu-messageformat-parser": 2.7.0
+ "@formatjs/intl-displaynames": 6.6.1
+ "@formatjs/intl-listformat": 7.5.0
+ intl-messageformat: 10.5.4
tslib: ^2.4.0
peerDependencies:
- typescript: ^4.7 || 5
+ typescript: 5
peerDependenciesMeta:
typescript:
optional: true
- checksum: 70df0ce7121e612f0415ed4dcc83d3d64499127c6a8996d30eb37c91d16d153721befc74332d5ab064b8b03c5bbd4073fd0b0f11f90a73559d997619e285eaac
+ checksum: 12f6ac51a6630a967dc2a446eab6bf2e1c046aa2a693c550c2fa594bcb095cecf2e38efcc1e07611261c00ffa58a2372d16460e4716fac073fc1b54d302e2520
languageName: node
linkType: hard
@@ -2736,6 +2743,17 @@ __metadata:
languageName: node
linkType: hard
+"@humanwhocodes/config-array@npm:^0.11.13":
+ version: 0.11.13
+ resolution: "@humanwhocodes/config-array@npm:0.11.13"
+ dependencies:
+ "@humanwhocodes/object-schema": ^2.0.1
+ debug: ^4.1.1
+ minimatch: ^3.0.5
+ checksum: f8ea57b0d7ed7f2d64cd3944654976829d9da91c04d9c860e18804729a33f7681f78166ef4c761850b8c324d362f7d53f14c5c44907a6b38b32c703ff85e4805
+ languageName: node
+ linkType: hard
+
"@humanwhocodes/module-importer@npm:^1.0.1":
version: 1.0.1
resolution: "@humanwhocodes/module-importer@npm:1.0.1"
@@ -2750,6 +2768,13 @@ __metadata:
languageName: node
linkType: hard
+"@humanwhocodes/object-schema@npm:^2.0.1":
+ version: 2.0.1
+ resolution: "@humanwhocodes/object-schema@npm:2.0.1"
+ checksum: 24929487b1ed48795d2f08346a0116cc5ee4634848bce64161fb947109352c562310fd159fc64dda0e8b853307f5794605191a9547f7341158559ca3c8262a45
+ languageName: node
+ linkType: hard
+
"@isaacs/cliui@npm:^8.0.2":
version: 8.0.2
resolution: "@isaacs/cliui@npm:8.0.2"
@@ -4046,10 +4071,10 @@ __metadata:
languageName: node
linkType: hard
-"@remix-run/router@npm:1.9.0":
- version: 1.9.0
- resolution: "@remix-run/router@npm:1.9.0"
- checksum: 0537b0ff29879ac85077cb4c42eaca4a295b9efd71477848984c2f2dfa5741c9b83d3106a7bb72994a51a9adfeeab3b0f5a40f2dee8be3f0750feeeca2a6d513
+"@remix-run/router@npm:1.10.0":
+ version: 1.10.0
+ resolution: "@remix-run/router@npm:1.10.0"
+ checksum: f8f9fcd5f08465a7e0a05378398ff6df2c5c5ef5766df3490a134d64260b3b16f1bd490bb0c3f5925c2671a0c1d8d1fa01dfbdc7ecc3b2447dc6eafe6b73bcc2
languageName: node
linkType: hard
@@ -4345,12 +4370,12 @@ __metadata:
languageName: node
linkType: hard
-"@types/dexie-batch@npm:0.4.5":
- version: 0.4.5
- resolution: "@types/dexie-batch@npm:0.4.5"
+"@types/dexie-batch@npm:0.4.6":
+ version: 0.4.6
+ resolution: "@types/dexie-batch@npm:0.4.6"
dependencies:
dexie: latest
- checksum: 827a71d6609b2f19a521b4cef3bb6b6cae932af18572237aa0599fce4ce1936ba398510e3b6da5b2e783844f096b3dc2b5b8b942c8569aa22e37c4d187fa5594
+ checksum: 59ef6b3aa47b73a0c2f2b6badc6e32752b1dd8d7e985e5f9b86cfc16dd70af2846643a371152f385d9eeab3d087d5ea47a316e591a16b266abcc3f4a73dd9ba6
languageName: node
linkType: hard
@@ -4395,20 +4420,20 @@ __metadata:
languageName: node
linkType: hard
-"@types/fs-extra@npm:11.0.2":
- version: 11.0.2
- resolution: "@types/fs-extra@npm:11.0.2"
+"@types/fs-extra@npm:11.0.3":
+ version: 11.0.3
+ resolution: "@types/fs-extra@npm:11.0.3"
dependencies:
"@types/jsonfile": "*"
"@types/node": "*"
- checksum: 5b3e30343ee62d2e393e1029355f13f64bab6f3416226e22492483f99da840e2e53ca22cbfa4ac3749f2f83f7086d19c009005c8fa175da01df0fae59c2d73e1
+ checksum: f196bc216906e7016a6c9c549dbe204fe7e1e87515c7e961f741309e25f8e2f8c268dba3dbf0ca7f3ddab5911d39888472f8624ac0c11a461f1b2d05377e38fa
languageName: node
linkType: hard
-"@types/generate-changelog@npm:1.8.1":
- version: 1.8.1
- resolution: "@types/generate-changelog@npm:1.8.1"
- checksum: 97ec6317fbc001720c02b613b5ea52c0be7b4e9bc4639d27dd44baba2714cc45c96f08767e4fc7bcd042d1925193ad53223dc756f6b46132dab507d107bdf2c0
+"@types/generate-changelog@npm:1.8.2":
+ version: 1.8.2
+ resolution: "@types/generate-changelog@npm:1.8.2"
+ checksum: d2a063f45004c830b0b9395d9a0845e93f3408a7ec90e3e772eb750aa074b4482eba0f7b22eb68ce0d6bbf63457df818347196eb32d4f54588dc96d292c8b5e1
languageName: node
linkType: hard
@@ -4463,13 +4488,13 @@ __metadata:
languageName: node
linkType: hard
-"@types/jest@npm:29.5.5":
- version: 29.5.5
- resolution: "@types/jest@npm:29.5.5"
+"@types/jest@npm:29.5.6":
+ version: 29.5.6
+ resolution: "@types/jest@npm:29.5.6"
dependencies:
expect: ^29.0.0
pretty-format: ^29.0.0
- checksum: 56e55cde9949bcc0ee2fa34ce5b7c32c2bfb20e53424aa4ff3a210859eeaaa3fdf6f42f81a3f655238039cdaaaf108b054b7a8602f394e6c52b903659338d8c6
+ checksum: fa13a27bd1c8efd0381a419478769d0d6d3a8e93e1952d7ac3a16274e8440af6f73ed6f96ac1ff00761198badf2ee226b5ab5583a5d87a78d609ea78da5c5a24
languageName: node
linkType: hard
@@ -4491,21 +4516,21 @@ __metadata:
languageName: node
linkType: hard
-"@types/js-cookie@npm:3.0.4":
- version: 3.0.4
- resolution: "@types/js-cookie@npm:3.0.4"
- checksum: 46ac93974776a256f3cedadf60b45ded4d905a5e69986882d8c17baa351cb2e81a691864a1f19c3ca90eaa2cb3eeb7cb5426416b487a7d54cf5ff278d39d7870
+"@types/js-cookie@npm:3.0.5":
+ version: 3.0.5
+ resolution: "@types/js-cookie@npm:3.0.5"
+ checksum: 4d91ae26445499fdde283928aac9ad149be3561ef9b4d959f77e44694608accd5939c8c68ba42c50c2cfc007ccd442cc566a41077d7f2766390088fa91b612ce
languageName: node
linkType: hard
-"@types/jsdom@npm:21.1.3":
- version: 21.1.3
- resolution: "@types/jsdom@npm:21.1.3"
+"@types/jsdom@npm:21.1.4":
+ version: 21.1.4
+ resolution: "@types/jsdom@npm:21.1.4"
dependencies:
"@types/node": "*"
"@types/tough-cookie": "*"
parse5: ^7.0.0
- checksum: be8e42eb2d24db8abd3a19a229ce2c2e5d0809351765d9053272604ddf1df04fd16ff193eee9a2d79130952c4f91ee995148deae836a4d43451e64db8a698281
+ checksum: 915f619111dadd8d1bb7f12b6736c9d2e486911e1aed086de5fb003e7e40ae1e368da322dc04f2122ef47faf40ca75b9315ae2df3e8011f882dcf84660fb0d68
languageName: node
linkType: hard
@@ -4543,10 +4568,10 @@ __metadata:
languageName: node
linkType: hard
-"@types/keyboardjs@npm:2.5.1":
- version: 2.5.1
- resolution: "@types/keyboardjs@npm:2.5.1"
- checksum: 5fc7ab296af084cb23e87eeabc000dcec68a74e890f6acfddda914e918b7256aacd8ca09b730b2b0ac1645ee7a4178814e19e8ca01bc0d511647cb5af2127705
+"@types/keyboardjs@npm:2.5.2":
+ version: 2.5.2
+ resolution: "@types/keyboardjs@npm:2.5.2"
+ checksum: cdb9590e0ac0fcdc8599acfa3a1bd983c84d72d6c2f8cf2b27bf49f13c48de9fa56f7b68c9ed3e0951bc46fa4fd39ac2a1d76232b8fe5a20b28c57220ca996ae
languageName: node
linkType: hard
@@ -4566,13 +4591,20 @@ __metadata:
languageName: node
linkType: hard
-"@types/linkify-it@npm:*, @types/linkify-it@npm:3.0.3":
+"@types/linkify-it@npm:*":
version: 3.0.3
resolution: "@types/linkify-it@npm:3.0.3"
checksum: a734becc4e7476833b0e6951ec133c006a34809639c722d3e28b7cf88f5f6ccbb433f195788be5e56209b1e9e6e0778879291dd2db401acee3bb585c44dcc329
languageName: node
linkType: hard
+"@types/linkify-it@npm:3.0.4":
+ version: 3.0.4
+ resolution: "@types/linkify-it@npm:3.0.4"
+ checksum: cd873857faf77231811a5ee49aadffdbdd7c6309b92ca004cb28320993858d2e30cad7b343c6db928763ed0f766c6ed140e0f995536e488a1447a527b6f8127f
+ languageName: node
+ linkType: hard
+
"@types/loadable__component@npm:^5":
version: 5.13.5
resolution: "@types/loadable__component@npm:5.13.5"
@@ -4582,13 +4614,13 @@ __metadata:
languageName: node
linkType: hard
-"@types/markdown-it@npm:13.0.2":
- version: 13.0.2
- resolution: "@types/markdown-it@npm:13.0.2"
+"@types/markdown-it@npm:13.0.4":
+ version: 13.0.4
+ resolution: "@types/markdown-it@npm:13.0.4"
dependencies:
"@types/linkify-it": "*"
"@types/mdurl": "*"
- checksum: fe1f6a12ee8ad2246359376431a30d22c9b603e63e93e3e27d6920840934b9764034679a4d0b01ec54b0693c8d5c42012ec34715cba4f5b0736b8a4b66db4c74
+ checksum: 0af9c349467599f984e8faf548144e5c68ca8926d45e155cbc622897290fadb1fd31fced8194a2a1095406805dd21719d2bc74d8dc0295581d0eed771a4fd58b
languageName: node
linkType: hard
@@ -4616,7 +4648,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^20.8.6":
+"@types/node@npm:*, @types/node@npm:>=13.7.0":
version: 20.8.6
resolution: "@types/node@npm:20.8.6"
dependencies:
@@ -4639,6 +4671,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/node@npm:^20.8.7":
+ version: 20.8.7
+ resolution: "@types/node@npm:20.8.7"
+ dependencies:
+ undici-types: ~5.25.1
+ checksum: 2173c0c03daefcb60c03a61b1371b28c8fe412e7a40dc6646458b809d14a85fbc7aeb369d957d57f0aaaafd99964e77436f29b3b579232d8f2b20c58abbd1d25
+ languageName: node
+ linkType: hard
+
"@types/node@npm:~14":
version: 14.18.63
resolution: "@types/node@npm:14.18.63"
@@ -4653,12 +4694,12 @@ __metadata:
languageName: node
linkType: hard
-"@types/open-graph@npm:0.2.3":
- version: 0.2.3
- resolution: "@types/open-graph@npm:0.2.3"
+"@types/open-graph@npm:0.2.4":
+ version: 0.2.4
+ resolution: "@types/open-graph@npm:0.2.4"
dependencies:
"@types/cheerio": "*"
- checksum: 7d3cf515e10ff7c646c1f9da4c6a865e6b421841e8a2c68568ac21ef9c13210a68c8e7b695f0af77fd1dbc3dbdb3231c407281fbcb49ccda57991b78c11ab928
+ checksum: d073053a5566c0615f2aa5455c0d442d5bb6e6442452716f1aec97431ca63c3d5ad1917829285a47bf209de1c26cc57c29c94e7f9d3ed8216e7806129cc7d925
languageName: node
linkType: hard
@@ -4669,10 +4710,10 @@ __metadata:
languageName: node
linkType: hard
-"@types/platform@npm:1.3.4":
- version: 1.3.4
- resolution: "@types/platform@npm:1.3.4"
- checksum: dce83952e6af3e488fb2a3c7c7ec226cef4a8606ab63bd40301fe7883ff0c434512210497d6cb79adc4c8e671648ed3e7035438a40f41a5f395c60d2bfb6390d
+"@types/platform@npm:1.3.5":
+ version: 1.3.5
+ resolution: "@types/platform@npm:1.3.5"
+ checksum: acdefc3f58029747d76dc9f985ab1fc3dd62125d53fe807eedf2cf88f5c660e0aa31e4c2737cfe7738cf1972608ea03d06ca368ce1385cd5e5a5d54632d2a18c
languageName: node
linkType: hard
@@ -4692,7 +4733,16 @@ __metadata:
languageName: node
linkType: hard
-"@types/react-dom@npm:18.2.13, @types/react-dom@npm:^18.0.0":
+"@types/react-dom@npm:18.2.14":
+ version: 18.2.14
+ resolution: "@types/react-dom@npm:18.2.14"
+ dependencies:
+ "@types/react": "*"
+ checksum: 890289c70d1966c168037637c09cacefe6205bdd27a33252144a6b432595a2943775ac1a1accac0beddaeb67f8fdf721e076acb1adc990b08e51c3d9fd4e780c
+ languageName: node
+ linkType: hard
+
+"@types/react-dom@npm:^18.0.0":
version: 18.2.13
resolution: "@types/react-dom@npm:18.2.13"
dependencies:
@@ -4701,19 +4751,28 @@ __metadata:
languageName: node
linkType: hard
-"@types/react-redux@npm:7.1.27":
- version: 7.1.27
- resolution: "@types/react-redux@npm:7.1.27"
+"@types/react-redux@npm:7.1.28":
+ version: 7.1.28
+ resolution: "@types/react-redux@npm:7.1.28"
dependencies:
"@types/hoist-non-react-statics": ^3.3.0
"@types/react": "*"
hoist-non-react-statics: ^3.3.0
redux: ^4.0.0
- checksum: 38fcc56f013e81e9a3125fd75acdacb4cdb5f9fe49402330b4783923f236d2d12ccdd2240ffa42e5bbb75900acd55393c00e0ca5dd6cab91a7b7e39e74ac62b4
+ checksum: 521729d7ba781a700e39bf8f8723632ea0abe3698f43a7e131f7613f64613b5724b33ecb5446623ee698182407036f19fc56918c427d7b1fd05ad2a2bd9b6643
+ languageName: node
+ linkType: hard
+
+"@types/react-transition-group@npm:4.4.8":
+ version: 4.4.8
+ resolution: "@types/react-transition-group@npm:4.4.8"
+ dependencies:
+ "@types/react": "*"
+ checksum: ad7ba2bce97631fda9d89b4ed9772489bd050fec3ccd7563041b206dbe219d37d22e0d7731b1f90f56e89daf40e69ba16beba8066c42165bf8a584533feb6a2c
languageName: node
linkType: hard
-"@types/react-transition-group@npm:4.4.7, @types/react-transition-group@npm:^4.4.0":
+"@types/react-transition-group@npm:^4.4.0":
version: 4.4.7
resolution: "@types/react-transition-group@npm:4.4.7"
dependencies:
@@ -4733,12 +4792,12 @@ __metadata:
languageName: node
linkType: hard
-"@types/redux-mock-store@npm:1.0.4":
- version: 1.0.4
- resolution: "@types/redux-mock-store@npm:1.0.4"
+"@types/redux-mock-store@npm:1.0.5":
+ version: 1.0.5
+ resolution: "@types/redux-mock-store@npm:1.0.5"
dependencies:
redux: ^4.0.5
- checksum: 37a985fffe714cedefa9faaff1f0490e2d1638a2b926690a5e9680ed1aca89ae0a9a01f7ffa09951ed5c5b85e3fefaabcad86e0a502532e7028faf56c31ed6f3
+ checksum: 0ecae2d503f388c0e51e0b0eff10bedf3bfe331943c57b43efc66b84b1c1755079c736e8e099ab5be6336ff20d17a3377847c6d7abe39244cd78a69d0db3063d
languageName: node
linkType: hard
@@ -4795,10 +4854,10 @@ __metadata:
languageName: node
linkType: hard
-"@types/speakingurl@npm:13.0.4":
- version: 13.0.4
- resolution: "@types/speakingurl@npm:13.0.4"
- checksum: 1d9d0d5554ebbf8f1752be0c714680a49f414409053d19e7d5f83756c1f0bc2f7d483c46c92154585ac4e23576924585ae242f97ccaa80be0973d8afdbe512fb
+"@types/speakingurl@npm:13.0.5":
+ version: 13.0.5
+ resolution: "@types/speakingurl@npm:13.0.5"
+ checksum: 66b6ee271a684658be9e36625aa72fcc685ea329306f143e36b344cd55295cf384376a1d8942664d610153993b78acf66e06fe217b0e104a131204145b6747cb
languageName: node
linkType: hard
@@ -4823,10 +4882,10 @@ __metadata:
languageName: node
linkType: hard
-"@types/underscore@npm:1.11.11":
- version: 1.11.11
- resolution: "@types/underscore@npm:1.11.11"
- checksum: 56fbd04305d0982178f32fd4adcfcf6012f75005a3cf2e33eca7f0bf99dadc31c6c8c4884cb0d3896af16c4e06ccf02810305e970899d4766ec2760b19f61511
+"@types/underscore@npm:1.11.12":
+ version: 1.11.12
+ resolution: "@types/underscore@npm:1.11.12"
+ checksum: d9ee8505db35c9d07323613afb0df43197815efaeb34034ce2f37386ffd03637324bda27dab9d1ba7ac1c982824e1e3e1454f7d3ed2d56e0dd1d3d07959ecd4d
languageName: node
linkType: hard
@@ -4837,10 +4896,10 @@ __metadata:
languageName: node
linkType: hard
-"@types/webpack-env@npm:1.18.2":
- version: 1.18.2
- resolution: "@types/webpack-env@npm:1.18.2"
- checksum: 883908ade827d35a10efc574fb6f2728a7c520d4296cf1507633ac7457204ccd697bc6c8cadac99bc5d96074a6109c658ebfde59f42ba5ba0fdfffc538892b0f
+"@types/webpack-env@npm:1.18.3":
+ version: 1.18.3
+ resolution: "@types/webpack-env@npm:1.18.3"
+ checksum: f24e82485d8e325b1875608766ba6dad2b2f53a0fb182bc96173b4590110c6b791163402cdf19b50c04e8c05292f227a08aafe9230b2bba52c40c5f7ceccc101
languageName: node
linkType: hard
@@ -5045,6 +5104,13 @@ __metadata:
languageName: node
linkType: hard
+"@ungap/structured-clone@npm:^1.2.0":
+ version: 1.2.0
+ resolution: "@ungap/structured-clone@npm:1.2.0"
+ checksum: 4f656b7b4672f2ce6e272f2427d8b0824ed11546a601d8d5412b9d7704e83db38a8d9f402ecdf2b9063fc164af842ad0ec4a55819f621ed7e7ea4d1efcc74524
+ languageName: node
+ linkType: hard
+
"@webassemblyjs/ast@npm:1.11.6, @webassemblyjs/ast@npm:^1.11.5":
version: 1.11.6
resolution: "@webassemblyjs/ast@npm:1.11.6"
@@ -5250,10 +5316,10 @@ __metadata:
languageName: node
linkType: hard
-"@wireapp/avs@npm:9.4.18":
- version: 9.4.18
- resolution: "@wireapp/avs@npm:9.4.18"
- checksum: 6786018a880336bba8b1e00cc35de0b5834efedd3ecd8cc5c905fb40ef9cb3b8adc63d23bd202e3ba58e104fd4b9b011900d76f951e84e62de0bea0b5a8f32be
+"@wireapp/avs@npm:9.5.2":
+ version: 9.5.2
+ resolution: "@wireapp/avs@npm:9.5.2"
+ checksum: e728dfed25af97c6dee587a7f9174466971866993e098a0017eba37ed40a70b4a4155f4f9ab25968020e048ee1a31ced8ae9a224ee77cb793a556b40a01950af
languageName: node
linkType: hard
@@ -5435,9 +5501,9 @@ __metadata:
languageName: node
linkType: hard
-"@wireapp/react-ui-kit@npm:9.9.10":
- version: 9.9.10
- resolution: "@wireapp/react-ui-kit@npm:9.9.10"
+"@wireapp/react-ui-kit@npm:9.9.11":
+ version: 9.9.11
+ resolution: "@wireapp/react-ui-kit@npm:9.9.11"
dependencies:
"@types/color": 3.0.4
color: 4.2.3
@@ -5452,7 +5518,7 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
- checksum: 6946ac8f7be096e09d9abb8ad5d44d5fd0b4196f9e7581bd026ec04d941f12ec436dd6f9e6793658583ce2d9e494e1bc19bb587130967509685252d1a885d38e
+ checksum: 2bb5023b171b9b5da5084b783b8a55221f971b60acd8f7f8770f81578dd2edfa05ed8854c856235f0648231278762ac977305d602a953f31c89e762abb396b85
languageName: node
linkType: hard
@@ -7210,10 +7276,10 @@ __metadata:
languageName: node
linkType: hard
-"core-js@npm:3.33.0":
- version: 3.33.0
- resolution: "core-js@npm:3.33.0"
- checksum: dd62217935ac281faf6f833bb306fb891162919fcf9c1f0c975b1b91e82ac09a940f5deb5950bbb582739ceef716e8bd7e4f9eab8328932fb029d3bc2ecb2881
+"core-js@npm:3.33.1":
+ version: 3.33.1
+ resolution: "core-js@npm:3.33.1"
+ checksum: 3a95003b0e77995203587117f3bde7f4e96adf434b6b78033dbe60347ffe38b2bac31eafab6a4cc641e5766062846b52f336ab4553fc0902c278959af4778e53
languageName: node
linkType: hard
@@ -7266,10 +7332,10 @@ __metadata:
languageName: node
linkType: hard
-"countly-sdk-web@npm:23.6.1":
- version: 23.6.1
- resolution: "countly-sdk-web@npm:23.6.1"
- checksum: d519b155f4377dec56b9142f72e3c9cbb7a5614e7a1076b01b5afcb89ef956dc72fc1e960652c3b77bdb7772757468cb204543080bf4f284a876b5027df15b00
+"countly-sdk-web@npm:23.6.2":
+ version: 23.6.2
+ resolution: "countly-sdk-web@npm:23.6.2"
+ checksum: 37a40c7c48bd9c7f2d496a5a5f3e1f86370f3724a425f97f25423d8c013383c1e29978137fbc835da78d833233936e8625c4ae0a9adb6b0302062d9c00dda2f2
languageName: node
linkType: hard
@@ -8252,14 +8318,14 @@ __metadata:
languageName: node
linkType: hard
-"emoji-picker-react@npm:4.5.2":
- version: 4.5.2
- resolution: "emoji-picker-react@npm:4.5.2"
+"emoji-picker-react@npm:4.5.3":
+ version: 4.5.3
+ resolution: "emoji-picker-react@npm:4.5.3"
dependencies:
clsx: ^1.2.1
peerDependencies:
react: ">=16"
- checksum: b0f8aa3347cf804abcc209b5a707319d405f90bfc2a27a3b98900f2f8233b1a967328cca034b930d388d4a9480d7c12b2ded795976a554d9962eadaa607aaa06
+ checksum: 79681d0e4f3a733c9fa715559b8e3e6c20810e745844718d7072d909f9e5eb2870ba782df5d23d74e0b3ce6d733bbe045b42bf4b27bf1cfc0f5ecfbc971cfe61
languageName: node
linkType: hard
@@ -8912,7 +8978,7 @@ __metadata:
languageName: node
linkType: hard
-"eslint@npm:^8, eslint@npm:^8.51.0":
+"eslint@npm:^8":
version: 8.51.0
resolution: "eslint@npm:8.51.0"
dependencies:
@@ -8959,6 +9025,54 @@ __metadata:
languageName: node
linkType: hard
+"eslint@npm:^8.52.0":
+ version: 8.52.0
+ resolution: "eslint@npm:8.52.0"
+ dependencies:
+ "@eslint-community/eslint-utils": ^4.2.0
+ "@eslint-community/regexpp": ^4.6.1
+ "@eslint/eslintrc": ^2.1.2
+ "@eslint/js": 8.52.0
+ "@humanwhocodes/config-array": ^0.11.13
+ "@humanwhocodes/module-importer": ^1.0.1
+ "@nodelib/fs.walk": ^1.2.8
+ "@ungap/structured-clone": ^1.2.0
+ ajv: ^6.12.4
+ chalk: ^4.0.0
+ cross-spawn: ^7.0.2
+ debug: ^4.3.2
+ doctrine: ^3.0.0
+ escape-string-regexp: ^4.0.0
+ eslint-scope: ^7.2.2
+ eslint-visitor-keys: ^3.4.3
+ espree: ^9.6.1
+ esquery: ^1.4.2
+ esutils: ^2.0.2
+ fast-deep-equal: ^3.1.3
+ file-entry-cache: ^6.0.1
+ find-up: ^5.0.0
+ glob-parent: ^6.0.2
+ globals: ^13.19.0
+ graphemer: ^1.4.0
+ ignore: ^5.2.0
+ imurmurhash: ^0.1.4
+ is-glob: ^4.0.0
+ is-path-inside: ^3.0.3
+ js-yaml: ^4.1.0
+ json-stable-stringify-without-jsonify: ^1.0.1
+ levn: ^0.4.1
+ lodash.merge: ^4.6.2
+ minimatch: ^3.1.2
+ natural-compare: ^1.4.0
+ optionator: ^0.9.3
+ strip-ansi: ^6.0.1
+ text-table: ^0.2.0
+ bin:
+ eslint: bin/eslint.js
+ checksum: fd22d1e9bd7090e31b00cbc7a3b98f3b76020a4c4641f987ae7d0c8f52e1b88c3b268bdfdabac2e1a93513e5d11339b718ff45cbff48a44c35d7e52feba510ed
+ languageName: node
+ linkType: hard
+
"espree@npm:^9.0.0, espree@npm:^9.6.0, espree@npm:^9.6.1":
version: 9.6.1
resolution: "espree@npm:9.6.1"
@@ -10550,15 +10664,15 @@ __metadata:
languageName: node
linkType: hard
-"intl-messageformat@npm:10.5.3":
- version: 10.5.3
- resolution: "intl-messageformat@npm:10.5.3"
+"intl-messageformat@npm:10.5.4":
+ version: 10.5.4
+ resolution: "intl-messageformat@npm:10.5.4"
dependencies:
"@formatjs/ecma402-abstract": 1.17.2
"@formatjs/fast-memoize": 2.2.0
- "@formatjs/icu-messageformat-parser": 2.6.2
+ "@formatjs/icu-messageformat-parser": 2.7.0
tslib: ^2.4.0
- checksum: cc71cfa9b93aacba3f9d8ddcef7fab2bf81125de3edb7ee73b58185d107e86c301d54bd0e7a87141e311f5104ee90182f1ca52d8ee94446a0dfccc5c7db8541f
+ checksum: 1ac36b37134c435956762b5e9078314bb1d999992253c7ec884d135bf94eea160a3feede9d3c61c3b8a3016ca1553ba3fa3132bf95be4400c59ea95cb85a5ad6
languageName: node
linkType: hard
@@ -15281,27 +15395,27 @@ __metadata:
languageName: node
linkType: hard
-"react-intl@npm:6.4.7":
- version: 6.4.7
- resolution: "react-intl@npm:6.4.7"
+"react-intl@npm:6.5.1":
+ version: 6.5.1
+ resolution: "react-intl@npm:6.5.1"
dependencies:
"@formatjs/ecma402-abstract": 1.17.2
- "@formatjs/icu-messageformat-parser": 2.6.2
- "@formatjs/intl": 2.9.3
- "@formatjs/intl-displaynames": 6.5.2
- "@formatjs/intl-listformat": 7.4.2
+ "@formatjs/icu-messageformat-parser": 2.7.0
+ "@formatjs/intl": 2.9.5
+ "@formatjs/intl-displaynames": 6.6.1
+ "@formatjs/intl-listformat": 7.5.0
"@types/hoist-non-react-statics": ^3.3.1
"@types/react": 16 || 17 || 18
hoist-non-react-statics: ^3.3.2
- intl-messageformat: 10.5.3
+ intl-messageformat: 10.5.4
tslib: ^2.4.0
peerDependencies:
react: ^16.6.0 || 17 || 18
- typescript: ^4.7 || 5
+ typescript: 5
peerDependenciesMeta:
typescript:
optional: true
- checksum: 5b53096e4bf1e2a60b1175d381d645b284723280575054eba7c2f3a0b7aaf338228ffb7f1319caa7f7cdd7a463c9ba564e7ee09e3be794aa4c556fc850bbd670
+ checksum: 9cdf4549d490f34bc664923b3eaaf42aed8e3a9453ebdeffb96cf7a21f5db251b39ae2a3885cdb365579759f6e46ec2bb8f6717324851768efd77e2ccf7fc820
languageName: node
linkType: hard
@@ -15358,27 +15472,27 @@ __metadata:
languageName: node
linkType: hard
-"react-router-dom@npm:6.16.0":
- version: 6.16.0
- resolution: "react-router-dom@npm:6.16.0"
+"react-router-dom@npm:6.17.0":
+ version: 6.17.0
+ resolution: "react-router-dom@npm:6.17.0"
dependencies:
- "@remix-run/router": 1.9.0
- react-router: 6.16.0
+ "@remix-run/router": 1.10.0
+ react-router: 6.17.0
peerDependencies:
react: ">=16.8"
react-dom: ">=16.8"
- checksum: 18b398924bb0f0d97cf2f71dab71d860b960a7a267b2f77388990c662bb6d8738bdc3042d92f713fd63d269ae9ad90df39c8e97661b6ba758bbb7386b9f20ae0
+ checksum: e0ba4f4c507681e2ffdecdf2e67edf7ec0e2bf4be35222e29d013afdb03866a5e6ecacc8b452bd55797b9672785d02f81bd6dbf6b05ac93a59e48e774b0060de
languageName: node
linkType: hard
-"react-router@npm:6.16.0":
- version: 6.16.0
- resolution: "react-router@npm:6.16.0"
+"react-router@npm:6.17.0":
+ version: 6.17.0
+ resolution: "react-router@npm:6.17.0"
dependencies:
- "@remix-run/router": 1.9.0
+ "@remix-run/router": 1.10.0
peerDependencies:
react: ">=16.8"
- checksum: b31c187e3fdcdf7294ffdad6ff834e14d012840c94d8ee8c7fbe451062a8fcb6e31e8bc7827fb1ff45445dd40fad2b8c96a7e98f0ac1c3eb1d716c257a0821c9
+ checksum: 99c30d94fbb34657e4c8c3ef1aaae33b143167d3869b442e06c83b4006f35200fde810029180e209654bef2f47f0b27a928f77cc2d859a358a2722cc9d494f03
languageName: node
linkType: hard
@@ -18373,47 +18487,47 @@ __metadata:
"@emotion/eslint-plugin": ^11.11.0
"@emotion/react": 11.11.1
"@faker-js/faker": 8.1.0
- "@formatjs/cli": 6.2.0
+ "@formatjs/cli": 6.2.1
"@koush/wrtc": 0.5.3
"@lexical/history": 0.12.2
"@lexical/react": 0.12.2
"@peculiar/x509": 1.9.5
"@testing-library/react": 14.0.0
"@types/adm-zip": 0.5.2
- "@types/dexie-batch": 0.4.5
+ "@types/dexie-batch": 0.4.6
"@types/eslint": ^8
- "@types/fs-extra": 11.0.2
- "@types/generate-changelog": 1.8.1
- "@types/jest": 29.5.5
+ "@types/fs-extra": 11.0.3
+ "@types/generate-changelog": 1.8.2
+ "@types/jest": 29.5.6
"@types/jquery": ^3
- "@types/js-cookie": 3.0.4
- "@types/jsdom": 21.1.3
- "@types/keyboardjs": 2.5.1
+ "@types/js-cookie": 3.0.5
+ "@types/jsdom": 21.1.4
+ "@types/keyboardjs": 2.5.2
"@types/libsodium-wrappers": ^0
- "@types/linkify-it": 3.0.3
+ "@types/linkify-it": 3.0.4
"@types/loadable__component": ^5
- "@types/markdown-it": 13.0.2
- "@types/node": ^20.8.6
- "@types/open-graph": 0.2.3
- "@types/platform": 1.3.4
+ "@types/markdown-it": 13.0.4
+ "@types/node": ^20.8.7
+ "@types/open-graph": 0.2.4
+ "@types/platform": 1.3.5
"@types/react": 18.2.28
- "@types/react-dom": 18.2.13
- "@types/react-redux": 7.1.27
- "@types/react-transition-group": 4.4.7
- "@types/redux-mock-store": 1.0.4
+ "@types/react-dom": 18.2.14
+ "@types/react-redux": 7.1.28
+ "@types/react-transition-group": 4.4.8
+ "@types/redux-mock-store": 1.0.5
"@types/seedrandom": ^3
"@types/sinon": 10.0.19
- "@types/speakingurl": 13.0.4
- "@types/underscore": 1.11.11
- "@types/webpack-env": 1.18.2
- "@wireapp/avs": 9.4.18
+ "@types/speakingurl": 13.0.5
+ "@types/underscore": 1.11.12
+ "@types/webpack-env": 1.18.3
+ "@wireapp/avs": 9.5.2
"@wireapp/commons": 5.2.1
"@wireapp/copy-config": 2.1.9
"@wireapp/core": 42.17.0
"@wireapp/eslint-config": 3.0.4
"@wireapp/lru-cache": 3.8.1
"@wireapp/prettier-config": 0.6.3
- "@wireapp/react-ui-kit": 9.9.10
+ "@wireapp/react-ui-kit": 9.9.11
"@wireapp/store-engine": ^5.1.4
"@wireapp/store-engine-dexie": 2.1.6
"@wireapp/store-engine-sqleet": 1.8.9
@@ -18427,8 +18541,8 @@ __metadata:
beautiful-react-hooks: ^5.0.0
classnames: 2.3.2
copy-webpack-plugin: 11.0.0
- core-js: 3.33.0
- countly-sdk-web: 23.6.1
+ core-js: 3.33.1
+ countly-sdk-web: 23.6.2
cross-env: 7.0.3
cspell: 7.3.8
css-loader: ^6.8.1
@@ -18438,8 +18552,8 @@ __metadata:
dexie-batch: 0.4.3
dotenv: 16.3.1
dpdm: 3.14.0
- emoji-picker-react: 4.5.2
- eslint: ^8.51.0
+ emoji-picker-react: 4.5.3
+ eslint: ^8.52.0
eslint-plugin-prettier: ^5.0.1
fake-indexeddb: 4.0.2
generate-changelog: 1.8.0
@@ -18485,10 +18599,10 @@ __metadata:
react: 18.2.0
react-dom: 18.2.0
react-error-boundary: 4.0.11
- react-intl: 6.4.7
+ react-intl: 6.5.1
react-redux: 8.1.3
- react-router: 6.16.0
- react-router-dom: 6.16.0
+ react-router: 6.17.0
+ react-router-dom: 6.17.0
react-transition-group: 4.4.5
redux: 4.2.1
redux-devtools-extension: 2.13.9
@@ -18520,7 +18634,7 @@ __metadata:
webpack-hot-middleware: 2.25.4
webrtc-adapter: 8.2.3
workbox-webpack-plugin: 7.0.0
- zustand: 4.4.3
+ zustand: 4.4.4
languageName: unknown
linkType: soft
@@ -19034,9 +19148,9 @@ __metadata:
languageName: node
linkType: hard
-"zustand@npm:4.4.3":
- version: 4.4.3
- resolution: "zustand@npm:4.4.3"
+"zustand@npm:4.4.4":
+ version: 4.4.4
+ resolution: "zustand@npm:4.4.4"
dependencies:
use-sync-external-store: 1.2.0
peerDependencies:
@@ -19050,6 +19164,6 @@ __metadata:
optional: true
react:
optional: true
- checksum: 3ed16457a3a4b9fe6523f52d397af37db8fab5687dd21a23ede25f657346b25df374490baea27f10d416faae5e96acf7b4065c86044746d775881d266d1500f0
+ checksum: 371fd842dc704ed5983c6d64a77994c9c91867338c742d162ac95c4252b5f98fc38aeb2d5a07f48311babed5ca7dbff2d2258301db0ae143d32897bcf3ae651b
languageName: node
linkType: hard