From 7ba09df7995a46879282078ff7c0cea8c07d40e3 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Fri, 29 May 2026 21:29:53 +0200 Subject: [PATCH 1/4] fix: lazy initialize SignalingTypingHandler - while participants store is not migrated to pinia yet, eager import causing a dependencies loop (store -> pinia -> store), while they are not initialized yet AI-Assisted-By: claude-sonnet-4-6 Signed-off-by: Maksim Sukharev --- src/utils/webrtc/index.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/utils/webrtc/index.js b/src/utils/webrtc/index.js index 0b4c923cf59..126b9f2ecb8 100644 --- a/src/utils/webrtc/index.js +++ b/src/utils/webrtc/index.js @@ -40,7 +40,19 @@ let speakingStatusHandler = null // This does not really belongs here, as it is unrelated to WebRTC, but it is // included here for the time being until signaling and WebRTC are split. const enableTypingIndicators = getTalkConfig('local', 'chat', 'typing-privacy') === PRIVACY.PUBLIC -const signalingTypingHandler = enableTypingIndicators ? new SignalingTypingHandler(store) : null +let signalingTypingHandler = null +/** + * + */ +function getSignalingTypingHandler() { + if (!enableTypingIndicators) { + return null + } + if (!signalingTypingHandler) { + signalingTypingHandler = new SignalingTypingHandler() + } + return signalingTypingHandler +} let cancelFetchSignalingSettings = null let signaling = null @@ -131,7 +143,7 @@ async function connectSignaling(token) { signaling.setSettings(settings) }) - signalingTypingHandler?.setSignaling(signaling) + getSignalingTypingHandler()?.setSignaling(signaling) if (encryption) { encryption.close() @@ -545,7 +557,7 @@ async function signalingSendCallMessage(data) { * @param {boolean} typing whether the current participant is typing. */ function signalingSetTyping(typing) { - signalingTypingHandler?.setTyping(typing) + getSignalingTypingHandler()?.setTyping(typing) } export { From 06ce3311dff437f45a89173a4d22574320dc5893 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Fri, 29 May 2026 21:20:32 +0200 Subject: [PATCH 2/4] fix: migrate typing state to pinia AI-Assisted-By: claude-sonnet-4-6 Signed-off-by: Maksim Sukharev --- src/components/NewMessage/NewMessage.vue | 13 +- .../NewMessage/NewMessageTypingIndicator.vue | 7 +- src/store/participantsStore.js | 108 ----------------- src/stores/signalingState.ts | 114 ++++++++++++++++++ src/utils/SignalingTypingHandler.js | 21 ++-- src/utils/SignalingTypingHandler.spec.js | 60 +++++---- 6 files changed, 176 insertions(+), 147 deletions(-) create mode 100644 src/stores/signalingState.ts diff --git a/src/components/NewMessage/NewMessage.vue b/src/components/NewMessage/NewMessage.vue index 33945770401..26eaa615002 100644 --- a/src/components/NewMessage/NewMessage.vue +++ b/src/components/NewMessage/NewMessage.vue @@ -364,6 +364,7 @@ import { CONVERSATION, MESSAGE, PARTICIPANT, PRIVACY } from '../../constants.ts' import BrowserStorage from '../../services/BrowserStorage.js' import { getTalkConfig, hasTalkFeature } from '../../services/CapabilitiesManager.ts' import { EventBus } from '../../services/EventBus.ts' +import { setTyping } from '../../services/participantsService.js' import { useActorStore } from '../../stores/actor.ts' import { useChatStore } from '../../stores/chat.ts' import { useChatExtrasStore } from '../../stores/chatExtras.ts' @@ -937,12 +938,12 @@ export default { if (!this.typingInterval) { // Send first signal after first keystroke - this.$store.dispatch('sendTypingSignal', { typing: true }) + this.sendTypingSignal(true) // Continuously send signals with 10s interval if still typing this.typingInterval = setInterval(() => { if (this.wasTypingWithinInterval) { - this.$store.dispatch('sendTypingSignal', { typing: true }) + this.sendTypingSignal(true) this.wasTypingWithinInterval = false } else { this.clearTypingInterval() @@ -960,12 +961,18 @@ export default { }, resetTypingIndicator() { - this.$store.dispatch('sendTypingSignal', { typing: false }) + this.sendTypingSignal(false) if (this.typingInterval) { this.clearTypingInterval() } }, + async sendTypingSignal(isTyping) { + if (this.currentConversationIsJoined) { + await setTyping(isTyping) + } + }, + updateChatInput(text) { if (this.messageToEdit) { this.chatExtrasStore.setChatEditInput({ diff --git a/src/components/NewMessage/NewMessageTypingIndicator.vue b/src/components/NewMessage/NewMessageTypingIndicator.vue index 416fec97882..f5cfcca61a3 100644 --- a/src/components/NewMessage/NewMessageTypingIndicator.vue +++ b/src/components/NewMessage/NewMessageTypingIndicator.vue @@ -33,6 +33,7 @@ import AvatarWrapper from '../AvatarWrapper/AvatarWrapper.vue' import { AVATAR } from '../../constants.ts' import { useActorStore } from '../../stores/actor.ts' import { useGuestNameStore } from '../../stores/guestName.ts' +import { useSignalingStateStore } from '../../stores/signalingState.ts' export default { name: 'NewMessageTypingIndicator', @@ -50,9 +51,11 @@ export default { setup() { const guestNameStore = useGuestNameStore() + const signalingStateStore = useSignalingStateStore() return { AVATAR, guestNameStore, + signalingStateStore, actorStore: useActorStore(), } }, @@ -63,11 +66,11 @@ export default { }, externalTypingSignals() { - return this.$store.getters.externalTypingSignals(this.token) + return this.signalingStateStore.externalTypingSignals(this.token) }, typingParticipants() { - return this.$store.getters.participantsListTyping(this.token) + return this.signalingStateStore.participantsListTyping(this.token) }, visibleParticipants() { diff --git a/src/store/participantsStore.js b/src/store/participantsStore.js index 0579bc6fb85..305dfacccff 100644 --- a/src/store/participantsStore.js +++ b/src/store/participantsStore.js @@ -30,7 +30,6 @@ import { resendInvitations, sendCallNotification, setPermissions, - setTyping, } from '../services/participantsService.js' import SessionStorage from '../services/SessionStorage.js' import { talkBroadcastChannel } from '../services/talkBroadcastChannel.js' @@ -82,8 +81,6 @@ function state() { }, connectionFailed: { }, - typing: { - }, speaking: { }, // TODO: moved from callViewStore, separate to callExtras (with typing + speaking) @@ -130,56 +127,6 @@ const getters = { return [] }, - /** - * Gets the array of external session ids. - * - * @param {object} state - the state object. - * @return {Array} the typing session IDs array. - */ - externalTypingSignals: (state) => (token) => { - if (!state.typing[token]) { - return [] - } - const actorStore = useActorStore() - return Object.keys(state.typing[token]).filter((sessionId) => actorStore.sessionId !== sessionId) - }, - - /** - * Gets the array of external session ids. - * - * @param {object} state - the state object. - * @return {boolean} the typing status of actor. - */ - actorIsTyping: (state) => { - if (!state.typing[tokenStore.token]) { - return false - } - const actorStore = useActorStore() - return Object.keys(state.typing[tokenStore.token]).some((sessionId) => actorStore.sessionId === sessionId) - }, - - /** - * Gets the participants array filtered to include only those that are - * currently typing. - * - * @param {object} state - the state object. - * @param {object} getters - the getters object. - * @return {Array} the participants array (for registered users only). - */ - participantsListTyping: (state, getters) => (token) => { - if (!getters.externalTypingSignals(token).length) { - return [] - } - - const actorStore = useActorStore() - return getters.participantsList(token).filter((attendee) => { - // Check if participant's sessionId matches with any of sessionIds from signaling... - return getters.externalTypingSignals(token).some((sessionId) => attendee.sessionIds.includes(sessionId)) - // ... and it's not the participant with same actorType and actorId as yourself - && !actorStore.checkIfSelfIsActor(attendee) - }) - }, - /** * Gets the speaking information for the participant. * @@ -393,41 +340,6 @@ const mutations = { } }, - /** - * Sets the typing status of a participant in a conversation. - * - * Note that "updateParticipant" should not be called to add a "typing" - * property to an existing participant, as the participant would be reset - * when the participants are purged whenever they are fetched again. - * Similarly, "addParticipant" can not be called either to add a participant - * if it was not fetched yet but the signaling reported it as being typing, - * as the attendeeId would be unknown. - * - * @param {object} state - current store state. - * @param {object} data - the wrapping object. - * @param {string} data.token - the conversation that the participant is - * typing in. - * @param {string} data.sessionId - the Nextcloud session ID of the - * participant. - * @param {boolean} data.typing - whether the participant is typing or not. - * @param {number} data.expirationTimeout - id of timeout to watch for received signal expiration. - */ - setTyping(state, { token, sessionId, typing, expirationTimeout }) { - if (!state.typing[token]) { - state.typing[token] = {} - } - - if (state.typing[token][sessionId]) { - clearTimeout(state.typing[token][sessionId].expirationTimeout) - } - - if (typing) { - state.typing[token][sessionId] = { expirationTimeout } - } else { - delete state.typing[token][sessionId] - } - }, - /** * Sets the speaking status of a participant in a conversation / call. * @@ -1193,26 +1105,6 @@ const actions = { context.commit('updateParticipant', { token, attendeeId, updatedData }) }, - async sendTypingSignal(context, { typing }) { - if (!tokenStore.currentConversationIsJoined) { - return - } - - await setTyping(typing) - }, - - async setTyping(context, { token, sessionId, typing }) { - if (!typing) { - context.commit('setTyping', { token, sessionId, typing: false }) - } else { - const expirationTimeout = setTimeout(() => { - // If updated 'typing' signal doesn't come in last 15s, remove it from store - context.commit('setTyping', { token, sessionId, typing: false }) - }, 15000) - context.commit('setTyping', { token, sessionId, typing: true, expirationTimeout }) - } - }, - setSpeaking(context, { attendeeId, speaking }) { // We should update time before speaking state, to be able to check previous state context.commit('updateTimeSpeaking', { attendeeId, speaking }) diff --git a/src/stores/signalingState.ts b/src/stores/signalingState.ts new file mode 100644 index 00000000000..a4b13ac3bab --- /dev/null +++ b/src/stores/signalingState.ts @@ -0,0 +1,114 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { + Participant, +} from '../types/index.ts' + +import { defineStore } from 'pinia' +import { reactive } from 'vue' +import { useStore } from 'vuex' +import { useActorStore } from './actor.ts' + +type TypingState = { + expirationTimeout: ReturnType +} + +/** + * Store for signaling state used in chat and call (typing, speaking, raised hands) + */ +export const useSignalingStateStore = defineStore('signalingState', () => { + const typing = reactive>>({}) + + const actorStore = useActorStore() + const vuexStore = useStore() + + /** + * Get the array of external session ids for a conversation (excluding current user) + * + * @param token - conversation token + */ + function externalTypingSignals(token: string): string[] { + if (!typing[token]) { + return [] + } + + return Object.keys(typing[token]).filter((sessionId) => actorStore.sessionId !== sessionId) + } + + /** + * Check whether the current actor is typing in a conversation + * + * @param token - conversation token + */ + function isSelfActorTyping(token: string): boolean { + if (!typing[token]) { + return false + } + return Object.keys(typing[token]).some((sessionId) => actorStore.sessionId === sessionId) + } + + /** + * Get list of participants filtered to include only those that are currently typing + * + * @param token - conversation token + */ + function participantsListTyping(token: string): Participant[] { + const externalTypingIds = externalTypingSignals(token) + if (!externalTypingIds.length) { + return [] + } + + const participantsList = vuexStore.getters.participantsList(token) as Participant[] + return participantsList.filter((attendee) => { + // Check if participant's sessionId matches with any of sessionIds from signaling... + return externalTypingIds.some((sessionId) => attendee.sessionIds.includes(sessionId)) + // ... and it's not the participant with same actorType and actorId as yourself + && !actorStore.checkIfSelfIsActor(attendee) + }) + } + + /** + * Sets the typing status of a participant in a conversation. + * + * Note that "updateParticipant" should not be called to add a "typing" + * property to an existing participant, as the participant would be reset + * when the participants are purged whenever they are fetched again. + * Similarly, "addParticipant" can not be called either to add a participant + * if it was not fetched yet but the signaling reported it as being typing, + * as the attendeeId would be unknown. + * + * @param payload - the wrapping object. + * @param payload.token - the conversation that the participant is typing in. + * @param payload.sessionId - the Nextcloud session ID of the participant. + * @param payload.isTyping - whether the participant is typing or not. + */ + function setTyping({ token, sessionId, isTyping }: { token: string, sessionId: string, isTyping: boolean }) { + if (!typing[token]) { + typing[token] = {} + } + + if (typing[token][sessionId]) { + clearTimeout(typing[token][sessionId].expirationTimeout) + } + + if (isTyping) { + const expirationTimeout = setTimeout(() => { + // If updated 'typing' signal doesn't come in last 15s, remove it from store + setTyping({ token, sessionId, isTyping: false }) + }, 15000) + typing[token][sessionId] = { expirationTimeout } + } else { + delete typing[token][sessionId] + } + } + + return { + externalTypingSignals, + isSelfActorTyping, + participantsListTyping, + setTyping, + } +}) diff --git a/src/utils/SignalingTypingHandler.js b/src/utils/SignalingTypingHandler.js index 68dd2135977..60632820db3 100644 --- a/src/utils/SignalingTypingHandler.js +++ b/src/utils/SignalingTypingHandler.js @@ -5,6 +5,7 @@ import { useActorStore } from '../stores/actor.ts' import pinia from '../stores/pinia.ts' +import { useSignalingStateStore } from '../stores/signalingState.ts' import { useTokenStore } from '../stores/token.ts' import SignalingParticipantList from './SignalingParticipantList.js' @@ -15,12 +16,10 @@ import SignalingParticipantList from './SignalingParticipantList.js' * * It is expected that the typing status of the current participant will be * modified only when the current conversation is joined. - * - * @param {object} store the Vuex store */ -export default function SignalingTypingHandler(store) { - this._store = store +export default function SignalingTypingHandler() { this._actorStore = useActorStore(pinia) + this._signalingStateStore = useSignalingStateStore(pinia) this._tokenStore = useTokenStore(pinia) this._signaling = null @@ -92,10 +91,10 @@ SignalingTypingHandler.prototype = { }) } - this._store.dispatch('setTyping', { + this._signalingStateStore.setTyping({ token: this._tokenStore.token, sessionId: this._actorStore.sessionId, - typing, + isTyping: typing, }) }, @@ -109,15 +108,15 @@ SignalingTypingHandler.prototype = { return } - this._store.dispatch('setTyping', { + this._signalingStateStore.setTyping({ token: this._tokenStore.token, sessionId: participant.nextcloudSessionId, - typing: data.type === 'startedTyping', + isTyping: data.type === 'startedTyping', }) }, _handleParticipantsJoined(SignalingParticipantList, participants) { - if (!this._store.getters.actorIsTyping) { + if (!this._signalingStateStore.isSelfActorTyping(this._tokenStore.token)) { return } @@ -131,10 +130,10 @@ SignalingTypingHandler.prototype = { _handleParticipantsLeft(SignalingParticipantList, participants) { for (const participant of participants) { - this._store.dispatch('setTyping', { + this._signalingStateStore.setTyping({ token: this._tokenStore.token, sessionId: participant.nextcloudSessionId, - typing: false, + isTyping: false, }) } }, diff --git a/src/utils/SignalingTypingHandler.spec.js b/src/utils/SignalingTypingHandler.spec.js index 8514e287d8c..3f0eb93d9b8 100644 --- a/src/utils/SignalingTypingHandler.spec.js +++ b/src/utils/SignalingTypingHandler.spec.js @@ -8,12 +8,23 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import Vuex from 'vuex' import storeConfig from '../store/storeConfig.js' import { useActorStore } from '../stores/actor.ts' +import pinia from '../stores/pinia.ts' +import { useSignalingStateStore } from '../stores/signalingState.ts' import { useTokenStore } from '../stores/token.ts' import SignalingTypingHandler from './SignalingTypingHandler.js' +let store +vi.mock('vuex', async () => { + const vuex = await vi.importActual('vuex') + return { + ...vuex, + useStore: vi.fn(() => store), + } +}) + describe('SignalingTypingHandler', () => { - let store let actorStore + let signalingStateStore let tokenStore let signaling @@ -86,6 +97,7 @@ describe('SignalingTypingHandler', () => { }) store = new Vuex.Store(testStoreConfig) actorStore = useActorStore() + signalingStateStore = useSignalingStateStore() tokenStore = useTokenStore() signaling = new function() { @@ -125,7 +137,7 @@ describe('SignalingTypingHandler', () => { this.emit = vi.fn() }() - signalingTypingHandler = new SignalingTypingHandler(store) + signalingTypingHandler = new SignalingTypingHandler() signalingTypingHandler._signalingParticipantList.getParticipants = vi.fn() actorStore.setCurrentParticipant({ @@ -138,6 +150,8 @@ describe('SignalingTypingHandler', () => { vi.clearAllMocks() tokenStore.token = '' tokenStore.lastJoinedConversationToken = null + // Dispose signalingStateStore so its setup re-runs and captures the new vuex store reference + useSignalingStateStore(pinia).$dispose() }) describe('start typing', () => { @@ -155,7 +169,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setTyping(true) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(0) }) @@ -175,7 +189,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setTyping(true) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(1) expect(signaling.emit).toHaveBeenCalledWith('message', { type: 'startedTyping', to: 'user1SignalingSessionId' }) }) @@ -198,7 +212,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setTyping(true) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(2) expect(signaling.emit).toHaveBeenNthCalledWith(1, 'message', { type: 'startedTyping', to: 'user1SignalingSessionId' }) expect(signaling.emit).toHaveBeenNthCalledWith(2, 'message', { type: 'startedTyping', to: 'guest1SignalingSessionId' }) @@ -215,7 +229,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setSignaling(signaling) // Typing is not set once finally joined the room. - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(0) }) @@ -231,7 +245,7 @@ describe('SignalingTypingHandler', () => { tokenStore.updateLastJoinedConversationToken(TOKEN) // Typing is not set once finally joined the room. - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(0) }) }) @@ -252,7 +266,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setTyping(true) signalingTypingHandler.setTyping(false) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(0) }) @@ -273,7 +287,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setTyping(true) signalingTypingHandler.setTyping(false) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(2) expect(signaling.emit).toHaveBeenNthCalledWith(1, 'message', { type: 'startedTyping', to: 'user1SignalingSessionId' }) expect(signaling.emit).toHaveBeenNthCalledWith(2, 'message', { type: 'stoppedTyping', to: 'user1SignalingSessionId' }) @@ -298,7 +312,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setTyping(true) signalingTypingHandler.setTyping(false) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(4) expect(signaling.emit).toHaveBeenNthCalledWith(1, 'message', { type: 'startedTyping', to: 'user1SignalingSessionId' }) expect(signaling.emit).toHaveBeenNthCalledWith(2, 'message', { type: 'startedTyping', to: 'guest1SignalingSessionId' }) @@ -318,7 +332,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setSignaling(signaling) // Typing is not set once finally joined the room. - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(0) }) @@ -335,7 +349,7 @@ describe('SignalingTypingHandler', () => { tokenStore.updateLastJoinedConversationToken(TOKEN) // Typing is not set once finally joined the room. - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(0) }) }) @@ -362,7 +376,7 @@ describe('SignalingTypingHandler', () => { from: 'user1SignalingSessionId', }]) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([ + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([ expectedUser1Participant, ]) }) @@ -390,7 +404,7 @@ describe('SignalingTypingHandler', () => { from: 'user1SignalingSessionId', }]) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) }) }) @@ -420,7 +434,7 @@ describe('SignalingTypingHandler', () => { from: 'user1SignalingSessionId', }]) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) }) test('in another room', () => { @@ -450,7 +464,7 @@ describe('SignalingTypingHandler', () => { from: 'user1SignalingSessionId', }]) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) }) }) @@ -474,7 +488,7 @@ describe('SignalingTypingHandler', () => { localParticipantInSignalingParticipantList, ]]) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(1) expect(signaling.emit).toHaveBeenCalledWith('message', { type: 'startedTyping', to: 'user1SignalingSessionId' }) }) @@ -501,7 +515,7 @@ describe('SignalingTypingHandler', () => { user1ParticipantInSignalingParticipantList, ]]) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(2) expect(signaling.emit).toHaveBeenNthCalledWith(1, 'message', { type: 'startedTyping', to: 'guest1SignalingSessionId' }) expect(signaling.emit).toHaveBeenNthCalledWith(2, 'message', { type: 'startedTyping', to: 'user1SignalingSessionId' }) @@ -530,7 +544,7 @@ describe('SignalingTypingHandler', () => { user1ParticipantInSignalingParticipantList, ]]) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(2) expect(signaling.emit).toHaveBeenNthCalledWith(1, 'message', { type: 'startedTyping', to: 'guest1SignalingSessionId' }) expect(signaling.emit).toHaveBeenNthCalledWith(2, 'message', { type: 'stoppedTyping', to: 'guest1SignalingSessionId' }) @@ -568,7 +582,7 @@ describe('SignalingTypingHandler', () => { guest1ParticipantInSignalingParticipantList, ]]) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([ + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([ expectedGuest2Participant, ]) }) @@ -609,7 +623,7 @@ describe('SignalingTypingHandler', () => { guest1ParticipantInSignalingParticipantList, ]]) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([ + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([ expectedGuest2Participant, ]) }) @@ -633,7 +647,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setTyping(true) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(0) }) @@ -660,7 +674,7 @@ describe('SignalingTypingHandler', () => { from: 'user1SignalingSessionId', }]) - expect(store.getters.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) }) }) }) From 6b36c9020f26e23afcf1f0cb47c980acb18edec8 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Mon, 15 Jun 2026 11:41:55 +0200 Subject: [PATCH 3/4] Revert "fix: lazy initialize SignalingTypingHandler" This reverts commit 7ba09df7995a46879282078ff7c0cea8c07d40e3. --- src/utils/webrtc/index.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/utils/webrtc/index.js b/src/utils/webrtc/index.js index 126b9f2ecb8..0b4c923cf59 100644 --- a/src/utils/webrtc/index.js +++ b/src/utils/webrtc/index.js @@ -40,19 +40,7 @@ let speakingStatusHandler = null // This does not really belongs here, as it is unrelated to WebRTC, but it is // included here for the time being until signaling and WebRTC are split. const enableTypingIndicators = getTalkConfig('local', 'chat', 'typing-privacy') === PRIVACY.PUBLIC -let signalingTypingHandler = null -/** - * - */ -function getSignalingTypingHandler() { - if (!enableTypingIndicators) { - return null - } - if (!signalingTypingHandler) { - signalingTypingHandler = new SignalingTypingHandler() - } - return signalingTypingHandler -} +const signalingTypingHandler = enableTypingIndicators ? new SignalingTypingHandler(store) : null let cancelFetchSignalingSettings = null let signaling = null @@ -143,7 +131,7 @@ async function connectSignaling(token) { signaling.setSettings(settings) }) - getSignalingTypingHandler()?.setSignaling(signaling) + signalingTypingHandler?.setSignaling(signaling) if (encryption) { encryption.close() @@ -557,7 +545,7 @@ async function signalingSendCallMessage(data) { * @param {boolean} typing whether the current participant is typing. */ function signalingSetTyping(typing) { - getSignalingTypingHandler()?.setTyping(typing) + signalingTypingHandler?.setTyping(typing) } export { From 71e26e7408b2b0a71de3147642a959602279bedf Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Mon, 15 Jun 2026 11:56:37 +0200 Subject: [PATCH 4/4] fixup! move vuex <> pinia coupling to the only UI consumer Signed-off-by: Maksim Sukharev --- .../NewMessage/NewMessageTypingIndicator.vue | 14 +++++- src/stores/signalingState.ts | 27 ----------- src/utils/SignalingTypingHandler.spec.js | 48 +++++++++---------- 3 files changed, 37 insertions(+), 52 deletions(-) diff --git a/src/components/NewMessage/NewMessageTypingIndicator.vue b/src/components/NewMessage/NewMessageTypingIndicator.vue index f5cfcca61a3..bea58706ca3 100644 --- a/src/components/NewMessage/NewMessageTypingIndicator.vue +++ b/src/components/NewMessage/NewMessageTypingIndicator.vue @@ -69,8 +69,20 @@ export default { return this.signalingStateStore.externalTypingSignals(this.token) }, + /** + * Get list of participants filtered to include only those that are currently typing + */ typingParticipants() { - return this.signalingStateStore.participantsListTyping(this.token) + if (!this.externalTypingSignals.length) { + return [] + } + + return this.$store.getters.participantsList(this.token).filter((attendee) => { + // Check if participant's sessionId matches with any of sessionIds from signaling... + return this.externalTypingSignals.some((sessionId) => attendee.sessionIds.includes(sessionId)) + // ... and it's not the participant with same actorType and actorId as yourself + && !this.actorStore.checkIfSelfIsActor(attendee) + }) }, visibleParticipants() { diff --git a/src/stores/signalingState.ts b/src/stores/signalingState.ts index a4b13ac3bab..3a616be5a27 100644 --- a/src/stores/signalingState.ts +++ b/src/stores/signalingState.ts @@ -3,13 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { - Participant, -} from '../types/index.ts' - import { defineStore } from 'pinia' import { reactive } from 'vue' -import { useStore } from 'vuex' import { useActorStore } from './actor.ts' type TypingState = { @@ -23,7 +18,6 @@ export const useSignalingStateStore = defineStore('signalingState', () => { const typing = reactive>>({}) const actorStore = useActorStore() - const vuexStore = useStore() /** * Get the array of external session ids for a conversation (excluding current user) @@ -50,26 +44,6 @@ export const useSignalingStateStore = defineStore('signalingState', () => { return Object.keys(typing[token]).some((sessionId) => actorStore.sessionId === sessionId) } - /** - * Get list of participants filtered to include only those that are currently typing - * - * @param token - conversation token - */ - function participantsListTyping(token: string): Participant[] { - const externalTypingIds = externalTypingSignals(token) - if (!externalTypingIds.length) { - return [] - } - - const participantsList = vuexStore.getters.participantsList(token) as Participant[] - return participantsList.filter((attendee) => { - // Check if participant's sessionId matches with any of sessionIds from signaling... - return externalTypingIds.some((sessionId) => attendee.sessionIds.includes(sessionId)) - // ... and it's not the participant with same actorType and actorId as yourself - && !actorStore.checkIfSelfIsActor(attendee) - }) - } - /** * Sets the typing status of a participant in a conversation. * @@ -108,7 +82,6 @@ export const useSignalingStateStore = defineStore('signalingState', () => { return { externalTypingSignals, isSelfActorTyping, - participantsListTyping, setTyping, } }) diff --git a/src/utils/SignalingTypingHandler.spec.js b/src/utils/SignalingTypingHandler.spec.js index 3f0eb93d9b8..4eaeff12a9d 100644 --- a/src/utils/SignalingTypingHandler.spec.js +++ b/src/utils/SignalingTypingHandler.spec.js @@ -169,7 +169,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setTyping(true) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(0) }) @@ -189,7 +189,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setTyping(true) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(1) expect(signaling.emit).toHaveBeenCalledWith('message', { type: 'startedTyping', to: 'user1SignalingSessionId' }) }) @@ -212,7 +212,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setTyping(true) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(2) expect(signaling.emit).toHaveBeenNthCalledWith(1, 'message', { type: 'startedTyping', to: 'user1SignalingSessionId' }) expect(signaling.emit).toHaveBeenNthCalledWith(2, 'message', { type: 'startedTyping', to: 'guest1SignalingSessionId' }) @@ -229,7 +229,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setSignaling(signaling) // Typing is not set once finally joined the room. - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(0) }) @@ -245,7 +245,7 @@ describe('SignalingTypingHandler', () => { tokenStore.updateLastJoinedConversationToken(TOKEN) // Typing is not set once finally joined the room. - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(0) }) }) @@ -266,7 +266,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setTyping(true) signalingTypingHandler.setTyping(false) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(0) }) @@ -287,7 +287,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setTyping(true) signalingTypingHandler.setTyping(false) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(2) expect(signaling.emit).toHaveBeenNthCalledWith(1, 'message', { type: 'startedTyping', to: 'user1SignalingSessionId' }) expect(signaling.emit).toHaveBeenNthCalledWith(2, 'message', { type: 'stoppedTyping', to: 'user1SignalingSessionId' }) @@ -312,7 +312,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setTyping(true) signalingTypingHandler.setTyping(false) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(4) expect(signaling.emit).toHaveBeenNthCalledWith(1, 'message', { type: 'startedTyping', to: 'user1SignalingSessionId' }) expect(signaling.emit).toHaveBeenNthCalledWith(2, 'message', { type: 'startedTyping', to: 'guest1SignalingSessionId' }) @@ -332,7 +332,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setSignaling(signaling) // Typing is not set once finally joined the room. - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(0) }) @@ -349,7 +349,7 @@ describe('SignalingTypingHandler', () => { tokenStore.updateLastJoinedConversationToken(TOKEN) // Typing is not set once finally joined the room. - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(0) }) }) @@ -376,8 +376,8 @@ describe('SignalingTypingHandler', () => { from: 'user1SignalingSessionId', }]) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([ - expectedUser1Participant, + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([ + expectedUser1Participant.sessionIds[0], ]) }) @@ -404,7 +404,7 @@ describe('SignalingTypingHandler', () => { from: 'user1SignalingSessionId', }]) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) }) }) @@ -434,7 +434,7 @@ describe('SignalingTypingHandler', () => { from: 'user1SignalingSessionId', }]) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) }) test('in another room', () => { @@ -464,7 +464,7 @@ describe('SignalingTypingHandler', () => { from: 'user1SignalingSessionId', }]) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) }) }) @@ -488,7 +488,7 @@ describe('SignalingTypingHandler', () => { localParticipantInSignalingParticipantList, ]]) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(1) expect(signaling.emit).toHaveBeenCalledWith('message', { type: 'startedTyping', to: 'user1SignalingSessionId' }) }) @@ -515,7 +515,7 @@ describe('SignalingTypingHandler', () => { user1ParticipantInSignalingParticipantList, ]]) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(2) expect(signaling.emit).toHaveBeenNthCalledWith(1, 'message', { type: 'startedTyping', to: 'guest1SignalingSessionId' }) expect(signaling.emit).toHaveBeenNthCalledWith(2, 'message', { type: 'startedTyping', to: 'user1SignalingSessionId' }) @@ -544,7 +544,7 @@ describe('SignalingTypingHandler', () => { user1ParticipantInSignalingParticipantList, ]]) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(2) expect(signaling.emit).toHaveBeenNthCalledWith(1, 'message', { type: 'startedTyping', to: 'guest1SignalingSessionId' }) expect(signaling.emit).toHaveBeenNthCalledWith(2, 'message', { type: 'stoppedTyping', to: 'guest1SignalingSessionId' }) @@ -582,8 +582,8 @@ describe('SignalingTypingHandler', () => { guest1ParticipantInSignalingParticipantList, ]]) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([ - expectedGuest2Participant, + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([ + expectedGuest2Participant.sessionIds[0], ]) }) @@ -623,8 +623,8 @@ describe('SignalingTypingHandler', () => { guest1ParticipantInSignalingParticipantList, ]]) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([ - expectedGuest2Participant, + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([ + expectedGuest2Participant.sessionIds[0], ]) }) @@ -647,7 +647,7 @@ describe('SignalingTypingHandler', () => { signalingTypingHandler.setTyping(true) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) expect(signaling.emit).toHaveBeenCalledTimes(0) }) @@ -674,7 +674,7 @@ describe('SignalingTypingHandler', () => { from: 'user1SignalingSessionId', }]) - expect(signalingStateStore.participantsListTyping(TOKEN)).toEqual([]) + expect(signalingStateStore.externalTypingSignals(TOKEN)).toEqual([]) }) }) })