Skip to content

Commit

Permalink
Merge pull request #114 from GetStream/feature/reactions
Browse files Browse the repository at this point in the history
Implement reaction menu
  • Loading branch information
skydoves authored Oct 26, 2023
2 parents 3755e46 + d517d74 commit eb7304a
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 4 deletions.
1 change: 1 addition & 0 deletions features/video/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies {
implementation(project(":core:data"))

implementation(libs.stream.video.compose)
implementation(libs.stream.video.mock)

implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.androidx.lifecycle.viewModelCompose)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* Copyright 2023 Stream.IO, Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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,
ExperimentalLayoutApi::class,
ExperimentalLayoutApi::class
)

package io.getstream.whatsappclone.video

import android.content.res.Configuration
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.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 emoji 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 = VideoTheme.colors.barsBackground,
shape = RoundedCornerShape(2.dp)
)
.wrapContentWidth()
val onEmojiSelected: (emoji: String) -> Unit = {
sendReaction(scope, call, it, onDismiss)
}

Dialog(onDismiss) {
Card(
modifier = modifier.wrapContentWidth(),
backgroundColor = VideoTheme.colors.barsBackground
) {
Column(Modifier.padding(16.dp)) {
Row(horizontalArrangement = Arrangement.Center) {
ReactionItem(
modifier = Modifier
.background(
color = VideoTheme.colors.appBackground,
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}",
color = VideoTheme.colors.textHighEmphasis
)
}
}

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)
VideoTheme {
ReactionItem(
reactionMapper = ReactionMapper.defaultReactionMapper(),
onEmojiSelected = {
// Ignore
},
reaction = DefaultReactionsMenuData.mainReaction
)
}
}

@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ReactionMenuPreview() {
VideoTheme {
StreamMockUtils.initializeStreamVideo(LocalContext.current)
ReactionsMenu(
call = mockCall,
reactionMapper = ReactionMapper.defaultReactionMapper(),
onDismiss = { }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
Expand All @@ -39,6 +41,8 @@ import io.getstream.video.android.compose.ui.components.call.controls.actions.Le
import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleCameraAction
import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleMicrophoneAction
import io.getstream.video.android.core.Call
import io.getstream.video.android.mock.StreamMockUtils
import io.getstream.video.android.mock.mockCall
import io.getstream.whatsappclone.designsystem.component.WhatsAppLoadingIndicator
import io.getstream.whatsappclone.uistate.WhatsAppVideoUiState

Expand Down Expand Up @@ -69,7 +73,7 @@ fun WhatsAppVideoCall(
}

@Composable
fun WhatsAppVideoCallContent(
private fun WhatsAppVideoCallContent(
call: Call,
videoCall: Boolean,
onBackPressed: () -> Unit
Expand Down Expand Up @@ -148,7 +152,7 @@ fun WhatsAppVideoCallContent(
}

@Composable
fun WhatsAppVideoCallError() {
private fun WhatsAppVideoCallError() {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
Expand All @@ -161,8 +165,20 @@ fun WhatsAppVideoCallError() {
}

@Composable
fun WhatsAppVideoLoading() {
private fun WhatsAppVideoLoading() {
Box(modifier = Modifier.fillMaxSize()) {
WhatsAppLoadingIndicator(modifier = Modifier.align(Alignment.Center))
}
}

@Preview
@Composable
private fun WhatsAppVideoCallContentPreview() {
StreamMockUtils.initializeStreamVideo(LocalContext.current)
VideoTheme {
WhatsAppVideoCallContent(
call = mockCall,
videoCall = true
) {}
}
}
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ androidxMacroBenchmark = "1.2.0"
androidxNavigation = "2.5.0"
androidxProfileinstaller = "1.3.1"
androidxStartup = "1.1.1"
androidxRoom = "2.5.2"
androidxRoom = "2.6.0"
androidxTest = "1.5.0"
androidxTestExt = "1.1.3"
androidxTracing = "1.1.0"
Expand Down Expand Up @@ -49,6 +49,7 @@ stream-chat-offline = { group = "io.getstream", name = "stream-chat-android-offl
stream-chat-compose = { group = "io.getstream", name = "stream-chat-android-compose", version.ref = "streamChatSDK" }
stream-video-core = { group = "io.getstream", name = "stream-video-android-core", version.ref = "streamVideoSDK" }
stream-video-compose = { group = "io.getstream", name = "stream-video-android-compose", version.ref = "streamVideoSDK" }
stream-video-mock = { group = "io.getstream", name = "stream-video-android-mock", version.ref = "streamVideoSDK" }
stream-log = { group = "io.getstream", name = "stream-log-android", version.ref = "streamLog" }
sealedx-core = { group = "com.github.skydoves", name = "sealedx-core", version.ref = "sealedx" }
sealedx-processor = { group = "com.github.skydoves", name = "sealedx-processor", version.ref = "sealedx" }
Expand Down

0 comments on commit eb7304a

Please sign in to comment.