@@ -20,6 +20,7 @@ package com.wire.android.ui.calling
2020
2121import android.view.View
2222import androidx.compose.runtime.getValue
23+ import androidx.compose.runtime.mutableStateMapOf
2324import androidx.compose.runtime.mutableStateOf
2425import androidx.compose.runtime.setValue
2526import androidx.lifecycle.ViewModel
@@ -29,26 +30,37 @@ import com.wire.android.mapper.UICallParticipantMapper
2930import com.wire.android.mapper.UserTypeMapper
3031import com.wire.android.media.CallRinger
3132import com.wire.android.model.ImageAsset
33+ import com.wire.android.ui.calling.model.InCallReaction
34+ import com.wire.android.ui.calling.model.ReactionSender
3235import com.wire.android.ui.calling.model.UICallParticipant
36+ import com.wire.android.ui.calling.ongoing.incallreactions.InCallReactions
37+ import com.wire.android.util.ExpiringMap
3338import com.wire.android.util.dispatchers.DispatcherProvider
39+ import com.wire.android.util.extension.withDelayAfterFirst
3440import com.wire.kalium.logic.data.call.Call
3541import com.wire.kalium.logic.data.call.ConversationTypeForCall
3642import com.wire.kalium.logic.data.call.VideoState
3743import com.wire.kalium.logic.data.conversation.Conversation
3844import com.wire.kalium.logic.data.conversation.ConversationDetails
3945import com.wire.kalium.logic.data.id.ConversationId
46+ import com.wire.kalium.logic.data.id.QualifiedID
47+ import com.wire.kalium.logic.data.user.UserId
4048import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase
4149import com.wire.kalium.logic.feature.call.usecase.FlipToBackCameraUseCase
4250import com.wire.kalium.logic.feature.call.usecase.FlipToFrontCameraUseCase
4351import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCase
4452import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallWithSortedParticipantsUseCase
53+ import com.wire.kalium.logic.feature.call.usecase.ObserveInCallReactionsUseCase
4554import com.wire.kalium.logic.feature.call.usecase.ObserveSpeakerUseCase
4655import com.wire.kalium.logic.feature.call.usecase.SetVideoPreviewUseCase
4756import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOffUseCase
4857import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOnUseCase
4958import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase
5059import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase
60+ import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase
5161import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase
62+ import com.wire.kalium.logic.feature.incallreaction.SendInCallReactionUseCase
63+ import com.wire.kalium.logic.functional.onSuccess
5264import com.wire.kalium.logic.util.PlatformView
5365import dagger.assisted.Assisted
5466import dagger.assisted.AssistedFactory
@@ -57,13 +69,18 @@ import dagger.hilt.android.lifecycle.HiltViewModel
5769import kotlinx.collections.immutable.persistentListOf
5870import kotlinx.collections.immutable.toPersistentList
5971import kotlinx.coroutines.CoroutineScope
72+ import kotlinx.coroutines.channels.BufferOverflow
73+ import kotlinx.coroutines.channels.Channel
6074import kotlinx.coroutines.flow.SharedFlow
6175import kotlinx.coroutines.flow.SharingStarted
6276import kotlinx.coroutines.flow.collectLatest
77+ import kotlinx.coroutines.flow.combine
6378import kotlinx.coroutines.flow.filterIsInstance
79+ import kotlinx.coroutines.flow.filterNotNull
6480import kotlinx.coroutines.flow.first
6581import kotlinx.coroutines.flow.flowOn
6682import kotlinx.coroutines.flow.map
83+ import kotlinx.coroutines.flow.receiveAsFlow
6784import kotlinx.coroutines.flow.shareIn
6885import kotlinx.coroutines.launch
6986
@@ -83,6 +100,9 @@ class SharedCallingViewModel @AssistedInject constructor(
83100 private val flipToFrontCamera : FlipToFrontCameraUseCase ,
84101 private val flipToBackCamera : FlipToBackCameraUseCase ,
85102 private val observeSpeaker : ObserveSpeakerUseCase ,
103+ private val observeInCallReactionsUseCase : ObserveInCallReactionsUseCase ,
104+ private val sendInCallReactionUseCase : SendInCallReactionUseCase ,
105+ private val getCurrentClientId : ObserveCurrentClientIdUseCase ,
86106 private val callRinger : CallRinger ,
87107 private val uiCallParticipantMapper : UICallParticipantMapper ,
88108 private val userTypeMapper : UserTypeMapper ,
@@ -93,6 +113,15 @@ class SharedCallingViewModel @AssistedInject constructor(
93113
94114 var participantsState by mutableStateOf(persistentListOf<UICallParticipant >())
95115
116+ private val _inCallReactions = Channel <InCallReaction >(
117+ capacity = 300 , // Max reactions to keep in queue
118+ onBufferOverflow = BufferOverflow .DROP_OLDEST ,
119+ )
120+
121+ val inCallReactions = _inCallReactions .receiveAsFlow().withDelayAfterFirst(InCallReactions .reactionsThrottleDelayMs)
122+
123+ val recentReactions = recentInCallReactionMap()
124+
96125 init {
97126 viewModelScope.launch {
98127 val allCallsSharedFlow = observeEstablishedCallWithSortedParticipants(conversationId)
@@ -110,6 +139,9 @@ class SharedCallingViewModel @AssistedInject constructor(
110139 launch {
111140 observeOnSpeaker(this )
112141 }
142+ launch {
143+ observeInCallReactions()
144+ }
113145 }
114146 }
115147
@@ -172,18 +204,22 @@ class SharedCallingViewModel @AssistedInject constructor(
172204 }
173205
174206 private suspend fun observeParticipants (sharedFlow : SharedFlow <Call ?>) {
175- sharedFlow.collectLatest { call ->
176- call?.let {
207+ combine(
208+ getCurrentClientId().filterNotNull(),
209+ sharedFlow.filterNotNull(),
210+ ) { clientId, call -> clientId to call }
211+ .collectLatest { (clientId, call) ->
177212 callState = callState.copy(
178- isMuted = it .isMuted,
179- callStatus = it .status,
180- isCameraOn = it .isCameraOn,
181- isCbrEnabled = it .isCbrEnabled && call.conversationType == Conversation .Type .ONE_ON_ONE ,
182- callerName = it .callerName,
213+ isMuted = call .isMuted,
214+ callStatus = call .status,
215+ isCameraOn = call .isCameraOn,
216+ isCbrEnabled = call .isCbrEnabled && call.conversationType == Conversation .Type .ONE_ON_ONE ,
217+ callerName = call .callerName,
183218 )
184- participantsState = call.participants.map { uiCallParticipantMapper.toUICallParticipant(it) }.toPersistentList()
219+ participantsState = call.participants.map {
220+ uiCallParticipantMapper.toUICallParticipant(it, clientId)
221+ }.toPersistentList()
185222 }
186- }
187223 }
188224
189225 fun hangUpCall (onCompleted : () -> Unit ) {
@@ -279,8 +315,42 @@ class SharedCallingViewModel @AssistedInject constructor(
279315 }
280316 }
281317
318+ private suspend fun observeInCallReactions () {
319+ observeInCallReactionsUseCase(conversationId).collect { message ->
320+
321+ val sender = participantsState.senderName(message.senderUserId)?.let { name ->
322+ ReactionSender .Other (name)
323+ } ? : ReactionSender .Unknown
324+
325+ message.emojis.forEach { emoji ->
326+ _inCallReactions .send(InCallReaction (emoji, sender))
327+ }
328+
329+ if (message.emojis.isNotEmpty()) {
330+ recentReactions.put(message.senderUserId, message.emojis.last())
331+ }
332+ }
333+ }
334+
335+ fun onReactionClick (emoji : String ) {
336+ viewModelScope.launch {
337+ sendInCallReactionUseCase(conversationId, emoji).onSuccess {
338+ _inCallReactions .send(InCallReaction (emoji, ReactionSender .You ))
339+ }
340+ }
341+ }
342+
343+ private fun recentInCallReactionMap (): MutableMap <UserId , String > =
344+ ExpiringMap <UserId , String >(
345+ scope = viewModelScope,
346+ expiration = InCallReactions .recentReactionShowDurationMs,
347+ delegate = mutableStateMapOf<UserId , String >()
348+ )
349+
282350 @AssistedFactory
283351 interface Factory {
284352 fun create (conversationId : ConversationId ): SharedCallingViewModel
285353 }
286354}
355+
356+ private fun List<UICallParticipant>.senderName (userId : QualifiedID ) = firstOrNull { it.id.value == userId.value }?.name
0 commit comments