Skip to content

Commit

Permalink
add frontend support for talking time
Browse files Browse the repository at this point in the history
Signed-off-by: Maksim Sukharev <[email protected]>
  • Loading branch information
Antreesy committed Jul 29, 2023
1 parent ff9b947 commit d2b54fe
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,11 @@ import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip.js'
import AvatarWrapper from '../../../../AvatarWrapper/AvatarWrapper.vue'
import ParticipantPermissionsEditor from './ParticipantPermissionsEditor/ParticipantPermissionsEditor.vue'
import { useIsInCall } from '../../../../../composables/useIsInCall.js'
import { CONVERSATION, PARTICIPANT, ATTENDEE } from '../../../../../constants.js'
import readableNumber from '../../../../../mixins/readableNumber.js'
import UserStatus from '../../../../../mixins/userStatus.js'
// Material design icons
import { formattedTime } from '../../../../../utils/formattedTime.js'
export default {
name: 'Participant',
Expand Down Expand Up @@ -298,11 +298,18 @@ export default {
emits: ['click-participant'],
setup() {
const isInCall = useIsInCall()
return { isInCall }
},
data() {
return {
isUserNameTooltipVisible: false,
isStatusTooltipVisible: false,
permissionsEditor: false,
speakingInterval: null,
timeSpeaking: null,
}
},
Expand Down Expand Up @@ -341,7 +348,9 @@ export default {
},
statusMessage() {
return this.getStatusMessage(this.participant)
return (this.isInCall && this.participant.inCall && this.timeSpeaking)
? '💬 ' + t('spreed', '{time} talking time', { time: formattedTime(this.timeSpeaking, true) })
: this.getStatusMessage(this.participant)
},
statusMessageTooltip() {
Expand Down Expand Up @@ -483,6 +492,14 @@ export default {
return this.participant.sessionIds || []
},
participantSpeakingInformation() {
return this.$store.getters.getParticipantSpeakingInformation(this.token, this.sessionIds)
},
participantSpeaking() {
return this.participantSpeakingInformation?.speaking
},
lastPing() {
return this.participant.lastPing
},
Expand Down Expand Up @@ -619,8 +636,31 @@ export default {
return ''
},
},
watch: {
participantSpeaking(value) {
if (value === true) {
if (!this.speakingInterval) {
this.speakingInterval = setInterval(this.computeElapsedTime, 1000)
}
} else {
clearInterval(this.speakingInterval)
this.speakingInterval = null
}
},
'participant.inCall'(value) {
if (!value) {
for (const sessionId of this.sessionIds) {
this.$store.dispatch('setSpeaking', { token: this.token, sessionId, speaking: false })
}
this.timeSpeaking = 0
}
},
},
methods: {
formattedTime,
updateUserNameNeedsTooltip() {
// check if ellipsized
const e = this.$refs.userName
Expand Down Expand Up @@ -724,6 +764,20 @@ export default {
showError(t('spreed', 'Could not modify permissions for {displayName}', { displayName: this.computedName }))
}
},
computeElapsedTime() {
if (!this.participantSpeakingInformation) {
return null
}
const { speaking, lastTimestamp, totalCountedTime } = this.participantSpeakingInformation
if (!speaking) {
this.timeSpeaking = totalCountedTime
} else {
this.timeSpeaking = Date.now() - lastTimestamp + totalCountedTime
}
},
},
}
</script>
Expand Down
3 changes: 3 additions & 0 deletions src/store/conversationsStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,9 @@ const actions = {
const response = await fetchConversation(token)
dispatch('updateTalkVersionHash', response)
dispatch('addConversation', response.data.ocs.data)
if (!response.data.ocs.data.callFlag) {
dispatch('purgeSpeakingStore', { token })
}
return response
} catch (error) {
if (error?.response) {
Expand Down
84 changes: 83 additions & 1 deletion src/store/participantsStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ const state = {
},
typing: {
},
speaking: {
},
}

const getters = {
Expand Down Expand Up @@ -138,6 +140,24 @@ const getters = {
})
},

/**
* Gets the speaking information for the participant.
*
* @param {object} state - the state object.
* param {string} token - the conversation token.
* param {Array<string>} sessionIds - session identifiers for the participant.
* @return {object|undefined}
*/
getParticipantSpeakingInformation: (state) => (token, sessionIds) => {
if (!state.speaking[token]) {
return false
}

// look for existing sessionId in the store
const sessionId = sessionIds.find(sessionId => state.speaking[token][sessionId])
return state.speaking[token][sessionId]
},

/**
* Replaces the legacy getParticipant getter. Returns a callback function in which you can
* pass in the token and attendeeId as arguments to get the participant object.
Expand Down Expand Up @@ -321,6 +341,57 @@ const mutations = {
}
},

/**
* Sets the speaking status of a participant in a conversation / call.
*
* Note that "updateParticipant" should not be called to add a "speaking"
* 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 speaking,
* as the attendeeId would be unknown.
*
* @param {object} state - current store state.
* @param {object} data - the wrapping object.
* @param {string} data.token - the conversation token participant is speaking in.
* @param {string} data.sessionId - the Nextcloud session ID of the participant.
* @param {boolean} data.speaking - whether the participant is speaking or not
*/
setSpeaking(state, { token, sessionId, speaking }) {
// create a dummy object for current call
if (!state.speaking[token]) {
Vue.set(state.speaking, token, {})
}
if (!state.speaking[token][sessionId]) {
Vue.set(state.speaking[token], sessionId, { speaking: null, lastTimestamp: 0, totalCountedTime: 0 })
}

const currentTimestamp = Date.now()
const currentSpeakingState = state.speaking[token][sessionId].speaking

// when stopped speaking, update the total talking time
if (!speaking) {
state.speaking[token][sessionId].totalCountedTime += (currentTimestamp - state.speaking[token][sessionId].lastTimestamp)
}

// don't change state for consecutive identical signals
if (currentSpeakingState !== speaking) {
state.speaking[token][sessionId].speaking = speaking
state.speaking[token][sessionId].lastTimestamp = currentTimestamp
}
},

/**
* Purge the speaking information (when the call ends).
*
* @param {object} state - current store state.
* @param {object} data - the wrapping object.
* @param {string} data.token - the conversation token.
*/
purgeSpeakingStore(state, { token }) {
Vue.delete(state.speaking, token)
},

/**
* Purge a given conversation from the previously added participants.
*
Expand Down Expand Up @@ -503,7 +574,7 @@ const actions = {
}, 10000)
},

async leaveCall({ commit, getters }, { token, participantIdentifier, all = false }) {
async leaveCall({ commit, dispatch, getters }, { token, participantIdentifier, all = false }) {
if (!participantIdentifier?.sessionId) {
console.error('Trying to leave call without sessionId')
}
Expand All @@ -526,6 +597,9 @@ const actions = {
sessionId: participantIdentifier.sessionId,
flags: PARTICIPANT.CALL_FLAG.DISCONNECTED,
})

// re-fetch conversation after leaving to update callFlag
await dispatch('fetchConversation', { token })
},

/**
Expand Down Expand Up @@ -750,6 +824,14 @@ const actions = {
context.commit('setTyping', { token, sessionId, typing: true, expirationTimeout })
}
},

setSpeaking(context, { token, sessionId, speaking }) {
context.commit('setSpeaking', { token, sessionId, speaking })
},

purgeSpeakingStore(context, { token }) {
context.commit('purgeSpeakingStore', { token })
},
}

export default { state, mutations, getters, actions }

0 comments on commit d2b54fe

Please sign in to comment.