Skip to content

Commit 863b835

Browse files
authored
Merge pull request #9431 from nextcloud/add-signaling-messages-for-typing-indicators
Add signaling messages for typing indicators
2 parents 98582a2 + 1509a36 commit 863b835

File tree

8 files changed

+1955
-2
lines changed

8 files changed

+1955
-2
lines changed

src/services/participantsService.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { PARTICIPANT } from '../constants.js'
2929
import {
3030
signalingJoinConversation,
3131
signalingLeaveConversation,
32+
signalingSetTyping,
3233
} from '../utils/webrtc/index.js'
3334

3435
const PERMISSIONS = PARTICIPANT.PERMISSIONS
@@ -220,6 +221,15 @@ const setPermissions = async (token, attendeeId, permission) => {
220221
})
221222
}
222223

224+
/**
225+
* Sets whether the current participant is typing or not.
226+
*
227+
* @param {boolean} typing whether the current participant is typing.
228+
*/
229+
const setTyping = (typing) => {
230+
signalingSetTyping(typing)
231+
}
232+
223233
export {
224234
joinConversation,
225235
rejoinConversation,
@@ -237,4 +247,5 @@ export {
237247
grantAllPermissionsToParticipant,
238248
removeAllPermissionsFromParticipant,
239249
setPermissions,
250+
setTyping,
240251
}

src/store/participantsStore.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
grantAllPermissionsToParticipant,
4343
removeAllPermissionsFromParticipant,
4444
setPermissions,
45+
setTyping,
4546
} from '../services/participantsService.js'
4647
import SessionStorage from '../services/SessionStorage.js'
4748

@@ -54,6 +55,8 @@ const state = {
5455
},
5556
connecting: {
5657
},
58+
typing: {
59+
},
5760
}
5861

5962
const getters = {
@@ -78,6 +81,31 @@ const getters = {
7881
return []
7982
},
8083

84+
/**
85+
* Gets the participants array filtered to include only those that are
86+
* currently typing.
87+
*
88+
* @param {object} state - the state object.
89+
* @param {object} getters - the getters object.
90+
* @return {Array} the participants array (if there are participants in the
91+
* store).
92+
*/
93+
participantsListTyping: (state, getters) => (token) => {
94+
if (!state.typing[token]) {
95+
return []
96+
}
97+
98+
return getters.participantsList(token).filter(attendee => {
99+
for (const sessionId of state.typing[token]) {
100+
if (attendee.sessionIds.includes(sessionId)) {
101+
return true
102+
}
103+
}
104+
105+
return false
106+
})
107+
},
108+
81109
/**
82110
* Replaces the legacy getParticipant getter. Returns a callback function in which you can
83111
* pass in the token and attendeeId as arguments to get the participant object.
@@ -226,6 +254,41 @@ const mutations = {
226254
}
227255
},
228256

257+
/**
258+
* Sets the typing status of a participant in a conversation.
259+
*
260+
* Note that "updateParticipant" should not be called to add a "typing"
261+
* property to an existing participant, as the participant would be reset
262+
* when the participants are purged whenever they are fetched again.
263+
* Similarly, "addParticipant" can not be called either to add a participant
264+
* if it was not fetched yet but the signaling reported it as being typing,
265+
* as the attendeeId would be unknown.
266+
*
267+
* @param {object} state - current store state.
268+
* @param {object} data - the wrapping object.
269+
* @param {string} data.token - the conversation that the participant is
270+
* typing in.
271+
* @param {string} data.sessionId - the Nextcloud session ID of the
272+
* participant.
273+
* @param {boolean} data.typing - whether the participant is typing or not.
274+
*/
275+
setTyping(state, { token, sessionId, typing }) {
276+
if (!typing) {
277+
if (state.typing[token] && state.typing[token].indexOf(sessionId) >= 0) {
278+
state.typing[token].splice(state.typing[token].indexOf(sessionId), 1)
279+
}
280+
281+
return
282+
}
283+
284+
if (!state.typing[token]) {
285+
Vue.set(state.typing, token, [])
286+
}
287+
if (!state.typing[token].includes(sessionId)) {
288+
state.typing[token].push(sessionId)
289+
}
290+
},
291+
229292
/**
230293
* Purge a given conversation from the previously added participants.
231294
*
@@ -635,6 +698,14 @@ const actions = {
635698
}
636699
context.commit('updateParticipant', { token, attendeeId, updatedData })
637700
},
701+
702+
async setTyping(context, { typing }) {
703+
if (!context.getters.currentConversationIsJoined) {
704+
return
705+
}
706+
707+
await setTyping(typing)
708+
},
638709
}
639710

640711
export default { state, mutations, getters, actions }

src/store/tokenStore.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const getters = {
4444
getFileIdForToken: (state) => () => {
4545
return state.fileIdForToken
4646
},
47-
currentConversationIsJoined() {
47+
currentConversationIsJoined: (state) => {
4848
return state.lastJoinedConversationToken === state.token
4949
},
5050
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
*
3+
* @copyright Copyright (c) 2023, Daniel Calviño Sánchez ([email protected])
4+
*
5+
* @license AGPL-3.0-or-later
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as
9+
* published by the Free Software Foundation, either version 3 of the
10+
* License, or (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*
20+
*/
21+
22+
import EmitterMixin from './EmitterMixin.js'
23+
24+
/**
25+
* Helper to keep track of the signaling participants.
26+
*
27+
* The participants included the following properties:
28+
* - nextcloudSessionId
29+
* - signalingSessionId
30+
* - userId (optional, not included if the participant is a guest)
31+
*
32+
* The following events are emitted:
33+
* - participantsJoined(participants)
34+
* - participantsLeft(participants)
35+
*/
36+
export default function SignalingParticipantList() {
37+
this._superEmitterMixin()
38+
39+
this._signaling = null
40+
this._participants = []
41+
42+
this._handleLeaveRoomBound = this._handleLeaveRoom.bind(this)
43+
this._handleUsersInRoomBound = this._handleUsersInRoom.bind(this)
44+
this._handleUsersJoinedBound = this._handleUsersJoined.bind(this)
45+
this._handleUsersLeftBound = this._handleUsersLeft.bind(this)
46+
}
47+
48+
SignalingParticipantList.prototype = {
49+
50+
destroy() {
51+
if (this._signaling) {
52+
this._signaling.off('leaveRoom', this._handleLeaveRoomBound)
53+
this._signaling.off('usersInRoom', this._handleUsersInRoomBound)
54+
this._signaling.off('usersJoined', this._handleUsersJoinedBound)
55+
this._signaling.off('usersLeft', this._handleUsersLeftBound)
56+
}
57+
58+
this._destroyed = true
59+
60+
this._participants = []
61+
},
62+
63+
setSignaling(signaling) {
64+
if (this._destroyed) {
65+
return
66+
}
67+
68+
if (this._signaling) {
69+
this._signaling.off('leaveRoom', this._handleLeaveRoomBound)
70+
this._signaling.off('usersInRoom', this._handleUsersInRoomBound)
71+
this._signaling.off('usersJoined', this._handleUsersJoinedBound)
72+
this._signaling.off('usersLeft', this._handleUsersLeftBound)
73+
}
74+
75+
this._signaling = signaling
76+
77+
if (this._signaling) {
78+
this._signaling.on('leaveRoom', this._handleLeaveRoomBound)
79+
this._signaling.on('usersInRoom', this._handleUsersInRoomBound)
80+
this._signaling.on('usersJoined', this._handleUsersJoinedBound)
81+
this._signaling.on('usersLeft', this._handleUsersLeftBound)
82+
}
83+
},
84+
85+
getParticipants() {
86+
return this._participants
87+
},
88+
89+
_handleLeaveRoom(token) {
90+
if (this._participants.length > 0) {
91+
this._trigger('participantsLeft', [this._participants])
92+
}
93+
94+
this._participants = []
95+
},
96+
97+
_handleUsersInRoom(users) {
98+
const participants = []
99+
const participantsJoined = []
100+
const participantsLeft = []
101+
102+
for (const user of users) {
103+
const participant = {
104+
nextcloudSessionId: user.sessionId,
105+
signalingSessionId: user.sessionId,
106+
}
107+
if (user.userId) {
108+
participant.userId = user.userId
109+
}
110+
111+
participants.push(participant)
112+
113+
if (!this._participants.find(oldParticipant => oldParticipant.signalingSessionId === participant.signalingSessionId)) {
114+
participantsJoined.push(participant)
115+
}
116+
}
117+
118+
for (const oldParticipant of this._participants) {
119+
if (!participants.find(participant => participant.signalingSessionId === oldParticipant.signalingSessionId)) {
120+
participantsLeft.push(oldParticipant)
121+
}
122+
}
123+
124+
this._participants = participants
125+
126+
if (participantsJoined.length > 0) {
127+
this._trigger('participantsJoined', [participantsJoined])
128+
}
129+
if (participantsLeft.length > 0) {
130+
this._trigger('participantsLeft', [participantsLeft])
131+
}
132+
},
133+
134+
_handleUsersJoined(users) {
135+
const participantsJoined = []
136+
137+
for (const user of users) {
138+
const participant = {
139+
nextcloudSessionId: user.roomsessionid,
140+
signalingSessionId: user.sessionid,
141+
}
142+
if (user.userid) {
143+
participant.userId = user.userid
144+
}
145+
146+
this._participants.push(participant)
147+
148+
participantsJoined.push(participant)
149+
}
150+
151+
if (participantsJoined.length > 0) {
152+
this._trigger('participantsJoined', [participantsJoined])
153+
}
154+
},
155+
156+
_handleUsersLeft(sessionIds) {
157+
const participantsLeft = []
158+
159+
for (const sessionId of sessionIds) {
160+
const index = this._participants.findIndex(participant => participant.signalingSessionId === sessionId)
161+
if (index >= 0) {
162+
participantsLeft.push(this._participants[index])
163+
164+
this._participants.splice(index, 1)
165+
}
166+
}
167+
168+
if (participantsLeft.length > 0) {
169+
this._trigger('participantsLeft', [participantsLeft])
170+
}
171+
},
172+
173+
}
174+
175+
EmitterMixin.apply(SignalingParticipantList.prototype)

0 commit comments

Comments
 (0)