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.