diff --git a/src/components/AvatarWrapper/AvatarWrapper.vue b/src/components/AvatarWrapper/AvatarWrapper.vue
index 4c625e7b93f..c9003308da8 100644
--- a/src/components/AvatarWrapper/AvatarWrapper.vue
+++ b/src/components/AvatarWrapper/AvatarWrapper.vue
@@ -25,6 +25,7 @@
'avatar-wrapper--offline': offline,
'avatar-wrapper--small': small,
'avatar-wrapper--condensed': condensed,
+ 'avatar-wrapper--highlighted': highlighted,
}"
:style="{'--condensed-overlap': condensedOverlap}">
diff --git a/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue b/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue
index 296517b38a8..6a161e3f402 100644
--- a/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue
+++ b/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue
@@ -40,6 +40,7 @@
disable-tooltip
:show-user-status="showUserStatus && !isSearched"
:preloaded-user-status="preloadedUserStatus"
+ :highlighted="isParticipantSpeaking"
:offline="isOffline" />
@@ -67,6 +68,7 @@
{{ statusMessage }}
@@ -230,11 +232,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',
@@ -298,11 +300,18 @@ export default {
emits: ['click-participant'],
+ setup() {
+ const isInCall = useIsInCall()
+ return { isInCall }
+ },
+
data() {
return {
isUserNameTooltipVisible: false,
isStatusTooltipVisible: false,
permissionsEditor: false,
+ speakingInterval: null,
+ timeSpeaking: null,
}
},
@@ -341,7 +350,13 @@ export default {
},
statusMessage() {
- return this.getStatusMessage(this.participant)
+ if (this.isInCall && this.participant.inCall && this.timeSpeaking) {
+ return this.isParticipantSpeaking
+ ? '💬 ' + t('spreed', '{time} talking …', { time: formattedTime(this.timeSpeaking, true) })
+ : '💬 ' + t('spreed', '{time} talking time', { time: formattedTime(this.timeSpeaking, true) })
+ } else {
+ return this.getStatusMessage(this.participant)
+ }
},
statusMessageTooltip() {
@@ -483,6 +498,14 @@ export default {
return this.participant.sessionIds || []
},
+ participantSpeakingInformation() {
+ return this.$store.getters.getParticipantSpeakingInformation(this.token, this.sessionIds)
+ },
+
+ isParticipantSpeaking() {
+ return this.participantSpeakingInformation?.speaking
+ },
+
lastPing() {
return this.participant.lastPing
},
@@ -619,8 +642,25 @@ export default {
return ''
},
},
+ watch: {
+ isParticipantSpeaking(speaking) {
+ if (speaking) {
+ if (!this.speakingInterval) {
+ this.speakingInterval = setInterval(this.computeElapsedTime, 1000)
+ }
+ } else {
+ if (speaking === undefined) {
+ this.timeSpeaking = 0
+ }
+ clearInterval(this.speakingInterval)
+ this.speakingInterval = null
+ }
+ },
+ },
methods: {
+ formattedTime,
+
updateUserNameNeedsTooltip() {
// check if ellipsized
const e = this.$refs.userName
@@ -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
+ }
+ },
},
}
@@ -789,6 +843,9 @@ export default {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ &--highlighted {
+ font-weight: bold;
+ }
}
&__icon {
width: 44px;
diff --git a/src/store/participantsStore.js b/src/store/participantsStore.js
index e8b96ffa677..7140e1fde26 100644
--- a/src/store/participantsStore.js
+++ b/src/store/participantsStore.js
@@ -140,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
} sessionIds - session identifiers for the participant.
+ * @return {object|undefined}
+ */
+ getParticipantSpeakingInformation: (state) => (token, sessionIds) => {
+ if (!state.speaking[token]) {
+ return undefined
+ }
+
+ // 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.