diff --git a/src/App.vue b/src/App.vue index ac50396e42..7430c5b86b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -192,6 +192,11 @@ export default { unsubscribe('notifications:action:execute', this.interceptNotificationActions) window.removeEventListener('beforeunload', this.preventUnload) + + EventBus.off('joined-conversation') + EventBus.off('switch-to-conversation') + EventBus.off('conversations-received') + EventBus.off('forbidden-route') }, beforeMount() { diff --git a/src/components/LeftSidebar/LeftSidebar.vue b/src/components/LeftSidebar/LeftSidebar.vue index bb97ed0c34..1ceb2b94b7 100644 --- a/src/components/LeftSidebar/LeftSidebar.vue +++ b/src/components/LeftSidebar/LeftSidebar.vue @@ -675,7 +675,7 @@ export default { this.debounceHandleScroll.clear?.() EventBus.off('should-refresh-conversations', this.handleShouldRefreshConversations) - EventBus.off('conversations-received', this.handleUnreadMention) + EventBus.off('conversations-received', this.handleConversationsReceived) EventBus.off('route-change', this.onRouteChange) this.cancelSearchPossibleConversations() diff --git a/src/services/EventBus.ts b/src/services/EventBus.ts index 32f0dc3a56..6d9ac15fc7 100644 --- a/src/services/EventBus.ts +++ b/src/services/EventBus.ts @@ -11,9 +11,13 @@ type GenericEventHandler = Handler | WildcardHandler & { once(type: Key, handler: Handler): void once(type: '*', handler: WildcardHandler): void + _onceHandlers: Map> } + export const EventBus: ExtendedEmitter = mitt() as ExtendedEmitter +EventBus._onceHandlers = new Map() + /** * Register a one-time event handler for the given type * @@ -27,7 +31,43 @@ EventBus.once = function(type: Key, handler: GenericEv const fn = (...args: Parameters) => { // @ts-expect-error: Vue: A spread argument must either have a tuple type or be passed to a rest parameter. handler(...args) - this.off(type, fn) + // @ts-expect-error: Vue: No overload matches this call. + this.off(type, handler) } this.on(type, fn) + + // Store reference to the original handler to be able to remove it later + if (!EventBus._onceHandlers.has(type)) { + EventBus._onceHandlers.set(type, new Map()) + } + EventBus._onceHandlers.get(type)!.set(handler, fn) +} + +const off = EventBus.off.bind(EventBus) +/** + * OVERRIDING OF ORIGINAL MITT FUNCTION + * Remove an event handler for the given type. + * If `handler` is omitted, all handlers of the given type are removed. + * @param type Type of event to unregister `handler` from (`'*'` to remove a wildcard handler) + * @param [handler] Handler function to remove + */ +EventBus.off = function(type: Key, handler?: GenericEventHandler) { + // @ts-expect-error: Vue: No overload matches this call + off(type, handler) + + if (!handler) { + EventBus._onceHandlers.delete(type) + return + } + + const typeOnceHandlers = EventBus._onceHandlers.get(type) + const onceHandler = typeOnceHandlers?.get(handler) + if (onceHandler) { + typeOnceHandlers!.delete(handler) + if (!typeOnceHandlers!.size) { + EventBus._onceHandlers.delete(type) + } + // @ts-expect-error: Vue: No overload matches this call + off(type, onceHandler) + } } diff --git a/src/services/__tests__/EventBus.spec.js b/src/services/__tests__/EventBus.spec.js new file mode 100644 index 0000000000..35c87d6303 --- /dev/null +++ b/src/services/__tests__/EventBus.spec.js @@ -0,0 +1,283 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { EventBus } from '../EventBus.ts' + +describe('EventBus', () => { + const customEvent1 = jest.fn() + const customEvent2 = jest.fn() + const customEvent3 = jest.fn() + const customEventOnce1 = jest.fn() + const customEventOnce2 = jest.fn() + const customEventOnce3 = jest.fn() + + const testEventBus = (type, handlers, onceHandlers) => { + expect(EventBus.all.get(type).length).toBe(handlers) + if (!onceHandlers) { + expect(EventBus._onceHandlers.get(type)).toBeUndefined() + } else { + expect(EventBus._onceHandlers.get(type).size).toBe(onceHandlers) + } + } + + afterEach(() => { + EventBus.all.clear() + jest.clearAllMocks() + }) + + describe('on and off', () => { + it('should emit and listen to custom events', () => { + // Arrange + EventBus.on('custom-event', customEvent1) + EventBus.on('custom-event', customEvent2) + + // Act + EventBus.emit('custom-event') + + // Assert + testEventBus('custom-event', 2) + expect(customEvent1).toHaveBeenCalledTimes(1) + expect(customEvent2).toHaveBeenCalledTimes(1) + }) + + it('should emit and listen to custom events with wildcard * ', () => { + // Arrange + EventBus.on('*', customEvent1) + EventBus.on('*', customEvent2) + + // Act + EventBus.emit('custom-event-1') + + // Assert + testEventBus('*', 2) + expect(customEvent1).toHaveBeenCalledTimes(1) + expect(customEvent2).toHaveBeenCalledTimes(1) + }) + + it('should remove listeners by given type and handler', () => { + // Arrange + EventBus.on('custom-event', customEvent1) + EventBus.on('custom-event', customEvent2) + EventBus.emit('custom-event') + testEventBus('custom-event', 2) + + // Act + EventBus.off('custom-event', customEvent1) + EventBus.emit('custom-event') + + // Assert + testEventBus('custom-event', 1) + expect(customEvent1).toHaveBeenCalledTimes(1) + expect(customEvent2).toHaveBeenCalledTimes(2) + }) + + it('should remove listeners by wildcard * and handler', () => { + // Arrange + EventBus.on('custom-event-1', customEvent1) + EventBus.on('custom-event-2', customEvent2) + EventBus.on('*', customEvent3) + EventBus.emit('custom-event-1') + EventBus.emit('custom-event-2') + testEventBus('custom-event-1', 1) + testEventBus('custom-event-2', 1) + testEventBus('*', 1) + expect(customEvent3).toHaveBeenCalledTimes(2) + + // Act + EventBus.off('*', customEvent3) + EventBus.emit('custom-event-1') + EventBus.emit('custom-event-2') + + // Assert + testEventBus('custom-event-1', 1) + testEventBus('custom-event-2', 1) + testEventBus('*', 0) + expect(customEvent1).toHaveBeenCalledTimes(2) + expect(customEvent2).toHaveBeenCalledTimes(2) + expect(customEvent3).toHaveBeenCalledTimes(2) + }) + + it('should remove listeners by given type only', () => { + // Arrange + EventBus.on('custom-event', customEvent1) + EventBus.on('custom-event', customEvent2) + EventBus.emit('custom-event') + testEventBus('custom-event', 2) + + // Act + EventBus.off('custom-event') + EventBus.emit('custom-event') + + // Assert + testEventBus('custom-event', 0) + expect(customEvent1).toHaveBeenCalledTimes(1) + expect(customEvent2).toHaveBeenCalledTimes(1) + }) + + it('should remove listeners by wildcard * only', () => { + // Arrange + EventBus.on('custom-event-1', customEvent1) + EventBus.on('custom-event-2', customEvent2) + EventBus.on('*', customEvent3) + EventBus.emit('custom-event-1') + EventBus.emit('custom-event-2') + testEventBus('custom-event-1', 1) + testEventBus('custom-event-2', 1) + testEventBus('*', 1) + expect(customEvent3).toHaveBeenCalledTimes(2) + + // Act + EventBus.off('*') + EventBus.emit('custom-event-1') + EventBus.emit('custom-event-2') + + // Assert + testEventBus('custom-event-1', 1) + testEventBus('custom-event-2', 1) + testEventBus('*', 0) + expect(customEvent1).toHaveBeenCalledTimes(2) + expect(customEvent2).toHaveBeenCalledTimes(2) + expect(customEvent3).toHaveBeenCalledTimes(2) + }) + }) + + describe('once and off', () => { + it('should emit and listen to custom events', () => { + // Arrange + EventBus.on('custom-event', customEvent1) + EventBus.once('custom-event', customEventOnce1) + EventBus.once('custom-event', customEventOnce2) + testEventBus('custom-event', 3, 2) + + // Act + EventBus.emit('custom-event') + EventBus.emit('custom-event') + + // Assert + expect(customEvent1).toHaveBeenCalledTimes(2) + expect(customEventOnce1).toHaveBeenCalledTimes(1) + expect(customEventOnce2).toHaveBeenCalledTimes(1) + testEventBus('custom-event', 1, 0) + }) + + it('should emit and listen to custom events with wildcard * ', () => { + // Arrange + EventBus.on('*', customEvent1) + EventBus.once('*', customEventOnce1) + EventBus.once('*', customEventOnce2) + testEventBus('*', 3, 2) + + // Act + EventBus.emit('custom-event-1') + EventBus.emit('custom-event-2') + + // Assert + expect(customEvent1).toHaveBeenCalledTimes(2) + expect(customEventOnce1).toHaveBeenCalledTimes(1) + expect(customEventOnce2).toHaveBeenCalledTimes(1) + testEventBus('*', 1, 0) + }) + + it('should remove listeners by given type and handler', () => { + // Arrange + EventBus.on('custom-event', customEvent1) + EventBus.once('custom-event', customEventOnce1) + EventBus.once('custom-event', customEventOnce2) + testEventBus('custom-event', 3, 2) + + // Act + EventBus.off('custom-event', customEventOnce1) + testEventBus('custom-event', 2, 1) + EventBus.emit('custom-event') + EventBus.emit('custom-event') + + // Assert + expect(customEvent1).toHaveBeenCalledTimes(2) + expect(customEventOnce1).toHaveBeenCalledTimes(0) + expect(customEventOnce2).toHaveBeenCalledTimes(1) + testEventBus('custom-event', 1, 0) + }) + + it('should remove listeners by wildcard * and handler', () => { + // Arrange + EventBus.once('custom-event-1', customEventOnce1) + EventBus.on('custom-event-2', customEvent2) + EventBus.once('custom-event-2', customEventOnce2) + EventBus.on('*', customEvent3) + EventBus.once('*', customEventOnce3) + testEventBus('custom-event-1', 1, 1) + testEventBus('custom-event-2', 2, 1) + testEventBus('*', 2, 1) + + // Act + EventBus.off('*', customEventOnce3) + testEventBus('custom-event-1', 1, 1) + testEventBus('custom-event-2', 2, 1) + testEventBus('*', 1, 0) + EventBus.emit('custom-event-1') + EventBus.emit('custom-event-2') + EventBus.emit('custom-event-3') + + // Assert + expect(customEventOnce1).toHaveBeenCalledTimes(1) + expect(customEvent2).toHaveBeenCalledTimes(1) + expect(customEventOnce2).toHaveBeenCalledTimes(1) + expect(customEvent3).toHaveBeenCalledTimes(3) + expect(customEventOnce3).toHaveBeenCalledTimes(0) + testEventBus('custom-event-1', 0, 0) + testEventBus('custom-event-2', 1, 0) + testEventBus('*', 1, 0) + }) + + it('should remove listeners by given type only', () => { + // Arrange + EventBus.on('custom-event', customEvent1) + EventBus.once('custom-event', customEventOnce1) + EventBus.once('custom-event', customEventOnce2) + testEventBus('custom-event', 3, 2) + + // Act + EventBus.off('custom-event') + testEventBus('custom-event', 0, 0) + EventBus.emit('custom-event') + EventBus.emit('custom-event') + + // Assert + expect(customEvent1).toHaveBeenCalledTimes(0) + expect(customEventOnce1).toHaveBeenCalledTimes(0) + expect(customEventOnce2).toHaveBeenCalledTimes(0) + }) + + it('should remove listeners by wildcard * only', () => { + // Arrange + EventBus.once('custom-event-1', customEventOnce1) + EventBus.on('custom-event-2', customEvent2) + EventBus.once('custom-event-2', customEventOnce2) + EventBus.on('*', customEvent3) + EventBus.once('*', customEventOnce3) + testEventBus('custom-event-1', 1, 1) + testEventBus('custom-event-2', 2, 1) + testEventBus('*', 2, 1) + + // Act + EventBus.off('*') + testEventBus('custom-event-1', 1, 1) + testEventBus('custom-event-2', 2, 1) + testEventBus('*', 0, 0) + EventBus.emit('custom-event-1') + EventBus.emit('custom-event-2') + EventBus.emit('custom-event-3') + + // Assert + expect(customEventOnce1).toHaveBeenCalledTimes(1) + expect(customEvent2).toHaveBeenCalledTimes(1) + expect(customEventOnce2).toHaveBeenCalledTimes(1) + expect(customEvent3).toHaveBeenCalledTimes(0) + expect(customEventOnce3).toHaveBeenCalledTimes(0) + testEventBus('custom-event-1', 0, 0) + testEventBus('custom-event-2', 1, 0) + testEventBus('*', 0, 0) + }) + }) +})