Skip to content

Commit

Permalink
Reaction dialog (#854)
Browse files Browse the repository at this point in the history
A new reaction dialog.
  • Loading branch information
aleksandar-apostolov authored Oct 11, 2023
1 parent d3207e0 commit d9b4484
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 },
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 */ },
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit d9b4484

Please sign in to comment.