diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt index 4812181229..7e36679168 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt @@ -77,6 +77,7 @@ fun CallScreen( val isMicrophoneEnabled by call.microphone.isEnabled.collectAsState() val speakingWhileMuted by call.state.speakingWhileMuted.collectAsState() var isShowingSettingMenu by remember { mutableStateOf(false) } + var isShowingReactionsMenu by remember { mutableStateOf(false) } var isShowingAvailableDeviceMenu by remember { mutableStateOf(false) } var unreadCount by remember { mutableIntStateOf(0) } val chatState = rememberModalBottomSheetState( @@ -250,6 +251,15 @@ fun CallScreen( showDebugOptions = showDebugOptions, onDisplayAvailableDevice = { isShowingAvailableDeviceMenu = true }, onDismissed = { isShowingSettingMenu = false }, + onShowReactionsMenu = { isShowingReactionsMenu = true }, + ) + } + + if (isShowingReactionsMenu) { + ReactionsMenu( + call = call, + reactionMapper = VideoTheme.reactionMapper, + onDismiss = { isShowingReactionsMenu = false }, ) } diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/ReactionsMenu.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/ReactionsMenu.kt new file mode 100644 index 0000000000..0eb6325cc7 --- /dev/null +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/ReactionsMenu.kt @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalLayoutApi::class) + +package io.getstream.video.android.ui.call + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import io.getstream.video.android.compose.theme.VideoTheme +import io.getstream.video.android.core.Call +import io.getstream.video.android.core.mapper.ReactionMapper +import io.getstream.video.android.mock.StreamMockUtils +import io.getstream.video.android.mock.mockCall +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Default reaction item data + * + * @param displayText the text visible on the screen. + * @param emojiCode the code of the emoju e.g. ":like:" + * */ +private data class ReactionItemData(val displayText: String, val emojiCode: String) + +/** + * Default defined reactions. + * + * There is one main reaction, and a list of other reactions. The main reaction is shown on top of the rest. + */ +private object DefaultReactionsMenuData { + val mainReaction = ReactionItemData("Raise hand", ":raise-hand:") + val defaultReactions = listOf( + ReactionItemData("Fireworks", ":fireworks:"), + ReactionItemData("Wave", ":hello:"), + ReactionItemData("Like", ":raise-hand:"), + ReactionItemData("Dislike", ":hate:"), + ReactionItemData("Smile", ":smile:"), + ReactionItemData("Heart", ":heart:"), + ) +} + +/** + * Reactions menu. The reaction menu is a dialog displaying the list of reactions found in + * [DefaultReactionsMenuData]. + * + * @param call the call object. + * @param reactionMapper the mapper of reactions to map from emoji code into UTF see: [ReactionMapper] + * @param onDismiss on dismiss listener. + */ +@Composable +internal fun ReactionsMenu( + call: Call, + reactionMapper: ReactionMapper, + onDismiss: () -> Unit, +) { + val scope = rememberCoroutineScope() + val modifier = Modifier + .background( + color = Color.White, + shape = RoundedCornerShape(2.dp), + ) + .wrapContentWidth() + val onEmojiSelected: (emoji: String) -> Unit = { + sendReaction(scope, call, it, onDismiss) + } + + Dialog(onDismiss) { + Card( + modifier = modifier.wrapContentWidth(), + ) { + Column(Modifier.padding(16.dp)) { + Row(horizontalArrangement = Arrangement.Center) { + ReactionItem( + modifier = Modifier + .background( + color = Color(0xFFF1F4F0), + shape = RoundedCornerShape(2.dp), + ) + .fillMaxWidth(), + textModifier = Modifier.fillMaxWidth(), + reactionMapper = reactionMapper, + reaction = DefaultReactionsMenuData.mainReaction, + onEmojiSelected = onEmojiSelected, + ) + } + FlowRow( + horizontalArrangement = Arrangement.Center, + maxItemsInEachRow = 3, + verticalArrangement = Arrangement.Center, + ) { + DefaultReactionsMenuData.defaultReactions.forEach { + ReactionItem( + modifier = modifier, + reactionMapper = reactionMapper, + onEmojiSelected = onEmojiSelected, + reaction = it, + ) + } + } + } + } + } +} + +@Composable +private fun ReactionItem( + modifier: Modifier = Modifier, + textModifier: Modifier = Modifier, + reactionMapper: ReactionMapper, + reaction: ReactionItemData, + onEmojiSelected: (emoji: String) -> Unit, +) { + val mappedEmoji = reactionMapper.map(reaction.emojiCode) + Box( + modifier = modifier + .clickable { + onEmojiSelected(reaction.emojiCode) + } + .padding(2.dp), + ) { + Text( + textAlign = TextAlign.Center, + modifier = textModifier.padding(12.dp), + text = "$mappedEmoji ${reaction.displayText}", + ) + } +} + +private fun sendReaction(scope: CoroutineScope, call: Call, emoji: String, onDismiss: () -> Unit) { + scope.launch { + call.sendReaction("default", emoji) + onDismiss() + } +} + +@Preview +@Composable +private fun ReactionItemPreview() { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + ReactionItem( + reactionMapper = ReactionMapper.defaultReactionMapper(), + onEmojiSelected = { + // Ignore + }, + reaction = DefaultReactionsMenuData.mainReaction, + ) +} + +@Preview +@Composable +private fun ReactionMenuPreview() { + VideoTheme { + StreamMockUtils.initializeStreamVideo(LocalContext.current) + ReactionsMenu( + call = mockCall, + reactionMapper = ReactionMapper.defaultReactionMapper(), + onDismiss = { /* Do nothing */ }, + ) + } +} diff --git a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/SettingsMenu.kt b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/SettingsMenu.kt index 54bd739346..6dcf2441a4 100644 --- a/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/SettingsMenu.kt +++ b/dogfooding/src/main/kotlin/io/getstream/video/android/ui/call/SettingsMenu.kt @@ -60,11 +60,10 @@ internal fun SettingsMenu( showDebugOptions: Boolean, onDisplayAvailableDevice: () -> Unit, onDismissed: () -> Unit, + onShowReactionsMenu: () -> Unit, ) { val context = LocalContext.current val scope = rememberCoroutineScope() - val reactions = - listOf(":fireworks:", ":hello:", ":raise-hand:", ":like:", ":hate:", ":smile:", ":heart:") val screenSharePermissionResult = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult(), @@ -99,11 +98,8 @@ internal fun SettingsMenu( ) { Row( modifier = Modifier.clickable { - scope.launch { - val shuffled = reactions.shuffled() - call.sendReaction(type = "default", emoji = shuffled.first()) - onDismissed.invoke() - } + onDismissed() + onShowReactionsMenu() }, ) { Icon(