From 2988ccd51bcddc916fbea292336cbd8ef8c8099b Mon Sep 17 00:00:00 2001 From: bqv Date: Sat, 7 Oct 2023 20:01:48 +0100 Subject: [PATCH] advanced message sending --- .../onlinego/data/model/local/Message.kt | 20 +-- .../data/model/ogs/AnalysisMessage.kt | 23 +++ .../onlinego/data/ogs/GameConnection.kt | 27 ++- .../onlinego/ui/screens/game/GameFragment.kt | 1 + .../onlinego/ui/screens/game/GameUI.kt | 17 +- .../onlinego/ui/screens/game/GameViewModel.kt | 26 ++- .../ui/screens/game/composables/ChatDialog.kt | 155 +++++++++++++++++- 7 files changed, 235 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/io/zenandroid/onlinego/data/model/ogs/AnalysisMessage.kt diff --git a/app/src/main/java/io/zenandroid/onlinego/data/model/local/Message.kt b/app/src/main/java/io/zenandroid/onlinego/data/model/local/Message.kt index 825e70d5..5f00ba3e 100644 --- a/app/src/main/java/io/zenandroid/onlinego/data/model/local/Message.kt +++ b/app/src/main/java/io/zenandroid/onlinego/data/model/local/Message.kt @@ -5,7 +5,7 @@ import androidx.room.Entity import androidx.room.Ignore import androidx.room.PrimaryKey import java.util.Locale -import io.zenandroid.onlinego.data.model.Cell +import io.zenandroid.onlinego.data.model.ogs.AnalysisMessage import io.zenandroid.onlinego.data.model.ogs.Chat import io.zenandroid.onlinego.data.model.ogs.ChatChannel import io.zenandroid.onlinego.gamelogic.Util @@ -113,21 +113,3 @@ data class Message ( } } } - -@Entity -data class AnalysisMessage ( - var name: String? = null, - var from: Int? = null, - var moves: List? = null, - @Ignore var marks: Map? = null, - @Ignore var pen_marks: List? = null, - @Ignore var branch_move: Long? = null, // deprecated -) { - @Ignore val type: String = "analysis" - - constructor(name: String?, from: Int?, moves: List?) : this() { - this.name = name - this.from = from - this.moves = moves - } -} diff --git a/app/src/main/java/io/zenandroid/onlinego/data/model/ogs/AnalysisMessage.kt b/app/src/main/java/io/zenandroid/onlinego/data/model/ogs/AnalysisMessage.kt new file mode 100644 index 00000000..8c6a9cf2 --- /dev/null +++ b/app/src/main/java/io/zenandroid/onlinego/data/model/ogs/AnalysisMessage.kt @@ -0,0 +1,23 @@ +package io.zenandroid.onlinego.data.model.ogs + +import androidx.room.Entity +import androidx.room.Ignore +import io.zenandroid.onlinego.data.model.Cell + +@Entity +data class AnalysisMessage ( + var name: String? = null, + var from: Int? = null, + var moves: List? = null, + @Ignore var marks: Map? = null, + @Ignore var pen_marks: List? = null, + @Ignore var branch_move: Long? = null, // deprecated +) { + @Ignore val type: String = "analysis" + + constructor(name: String?, from: Int?, moves: List?) : this() { + this.name = name + this.from = from + this.moves = moves + } +} diff --git a/app/src/main/java/io/zenandroid/onlinego/data/ogs/GameConnection.kt b/app/src/main/java/io/zenandroid/onlinego/data/ogs/GameConnection.kt index 2d0c99f9..201a43bf 100644 --- a/app/src/main/java/io/zenandroid/onlinego/data/ogs/GameConnection.kt +++ b/app/src/main/java/io/zenandroid/onlinego/data/ogs/GameConnection.kt @@ -10,6 +10,7 @@ import io.zenandroid.onlinego.data.model.Cell import io.zenandroid.onlinego.data.model.local.Message import io.zenandroid.onlinego.data.model.local.Score import io.zenandroid.onlinego.data.model.local.Time +import io.zenandroid.onlinego.data.model.ogs.AnalysisMessage import io.zenandroid.onlinego.data.model.ogs.Chat import io.zenandroid.onlinego.data.model.ogs.GameData import io.zenandroid.onlinego.data.model.ogs.OGSPlayer @@ -18,6 +19,7 @@ import io.zenandroid.onlinego.data.repositories.ChatRepository import io.zenandroid.onlinego.gamelogic.Util import io.zenandroid.onlinego.gamelogic.Util.getCurrentUserId import io.zenandroid.onlinego.utils.addToDisposable +import io.zenandroid.onlinego.utils.json import io.zenandroid.onlinego.utils.recordException import org.koin.core.context.GlobalContext.get import java.io.Closeable @@ -212,12 +214,33 @@ class GameConnection( } } - fun sendMessage(message: String, moveNumber: Int) { + fun sendMessage(message: String, moveNumber: Int, type: String = "main") { socketService.emit("game/chat") { "body" - message "game_id" - gameId "move_number" - moveNumber - "type" - "main" + "type" - type + } + } + + fun sendAnalysisMessage(message: AnalysisMessage, moveNumber: Int, type: String = "main") { + val message_moves = message.moves + ?.joinToString(separator = "") { + Util.getSGFCoordinates(it) + } + socketService.emit("game/chat") { + "body" - json { + "type" - message.type + "name" - message.name + "from" - message.from + "moves" - message_moves + "marks" - message.marks + "pen_marks" - message.pen_marks + "branch_move" - message.branch_move + } + "game_id" - gameId + "move_number" - moveNumber + "type" - type } } diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/GameFragment.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/GameFragment.kt index 800c9d1b..c0c3fbd8 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/GameFragment.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/GameFragment.kt @@ -68,6 +68,7 @@ class GameFragment : Fragment() { OnlineGoTheme { GameScreen( state = state, + analysisMode = viewModel.analyzeMode, onBack = ::onBackPressed, onUserAction = viewModel::onUserAction ) diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/GameUI.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/GameUI.kt index a41a6408..0636aa5e 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/GameUI.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/GameUI.kt @@ -58,6 +58,7 @@ 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.zenandroid.onlinego.data.model.ogs.ChatChannel import io.zenandroid.onlinego.data.model.Cell import io.zenandroid.onlinego.data.model.Position import io.zenandroid.onlinego.data.model.StoneType @@ -104,6 +105,7 @@ import io.zenandroid.onlinego.ui.screens.game.UserAction.RetryDialogDismiss import io.zenandroid.onlinego.ui.screens.game.UserAction.RetryDialogRetry import io.zenandroid.onlinego.ui.screens.game.UserAction.UserUndoDialogConfirm import io.zenandroid.onlinego.ui.screens.game.UserAction.UserUndoDialogDismiss +import io.zenandroid.onlinego.ui.screens.game.UserAction.VariationSend import io.zenandroid.onlinego.ui.screens.game.UserAction.WhitePlayerClicked import io.zenandroid.onlinego.ui.screens.game.composables.ChatDialog import io.zenandroid.onlinego.ui.screens.game.composables.PlayerCard @@ -112,6 +114,7 @@ import io.zenandroid.onlinego.ui.theme.OnlineGoTheme @Composable fun GameScreen(state: GameState, + analysisMode: Boolean, onUserAction: ((UserAction) -> Unit), onBack: (() -> Unit), ) { @@ -232,9 +235,11 @@ fun GameScreen(state: GameState, ChatDialog( messages = state.messages, game = state.position!!, + inAnalysisMode = analysisMode, + onVariation = { onUserAction(OpenVariation(it)) }, onDialogDismiss = { onUserAction(ChatDialogDismiss) }, - onSendMessage = { onUserAction(ChatSend(it)) }, - onVariation = { onUserAction(OpenVariation(it)) } + onSendMessage = { m, c -> onUserAction(ChatSend(m, c)) }, + onSendVariation = { onUserAction(VariationSend(it)) }, ) } if (state.retryMoveDialogShowing) { @@ -742,7 +747,7 @@ fun Preview() { blackStartTimer = null, timeLeft = 1000, ), - ), {}, {}, + ), false, {}, {}, ) } } @@ -785,7 +790,7 @@ fun Preview1() { blackStartTimer = null, timeLeft = 1000, ), - ), {}, {}, + ), false, {}, {}, ) } } @@ -829,6 +834,7 @@ fun Preview2() { ), bottomText = "Submitting move", ), + false, {}, {}, ) } @@ -873,6 +879,7 @@ fun Preview3() { bottomText = "Submitting move", retryMoveDialogShowing = true, ), + false, {}, {}, ) } @@ -919,6 +926,7 @@ fun Preview4() { showPlayers = false, showAnalysisPanel = true, ), + false, {}, {}, ) } @@ -975,6 +983,7 @@ fun Preview5() { } ), ), + false, {}, {}, ) } diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/GameViewModel.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/GameViewModel.kt index b2c4c5db..f3ba6379 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/GameViewModel.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/GameViewModel.kt @@ -17,6 +17,7 @@ import androidx.compose.material.icons.rounded.Undo import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -45,6 +46,8 @@ import io.zenandroid.onlinego.data.model.local.Player import io.zenandroid.onlinego.data.model.local.Score import io.zenandroid.onlinego.data.model.local.UserStats import io.zenandroid.onlinego.data.model.local.isPaused +import io.zenandroid.onlinego.data.model.ogs.AnalysisMessage +import io.zenandroid.onlinego.data.model.ogs.ChatChannel import io.zenandroid.onlinego.data.model.ogs.Phase import io.zenandroid.onlinego.data.model.ogs.VersusStats import io.zenandroid.onlinego.data.ogs.GameConnection @@ -104,6 +107,7 @@ import io.zenandroid.onlinego.ui.screens.game.UserAction.RetryDialogDismiss import io.zenandroid.onlinego.ui.screens.game.UserAction.RetryDialogRetry import io.zenandroid.onlinego.ui.screens.game.UserAction.UserUndoDialogConfirm import io.zenandroid.onlinego.ui.screens.game.UserAction.UserUndoDialogDismiss +import io.zenandroid.onlinego.ui.screens.game.UserAction.VariationSend import io.zenandroid.onlinego.ui.screens.game.UserAction.WhitePlayerClicked import io.zenandroid.onlinego.usecases.GetUserStatsUseCase import io.zenandroid.onlinego.usecases.RepoResult @@ -156,7 +160,8 @@ class GameViewModel( private var pendingMove by mutableStateOf(null) private var retrySendMoveDialogShowing by mutableStateOf(false) private var koMoveDialogShowing by mutableStateOf(false) - private var analyzeMode by mutableStateOf(false) + var analyzeMode by mutableStateOf(false) + private set private var estimateMode by mutableStateOf(false) private var analysisShownMoveNumber by mutableStateOf(0) private var passDialogShowing by mutableStateOf(false) @@ -296,7 +301,7 @@ class GameViewModel( estimateMode -> listOf(ExitEstimate) game?.phase == Phase.STONE_REMOVAL -> listOf(AcceptStoneRemoval, RejectStoneRemoval) gameFinished == true -> listOf(chatButton, Estimate(true), Previous, nextButton) - analyzeMode -> listOf(ExitAnalysis, Estimate(!isAnalysisDisabled()), Previous, nextButton) + analyzeMode -> listOf(ExitAnalysis, Estimate(!isAnalysisDisabled()), chatButton, Previous, nextButton) pendingMove != null -> emptyList() isMyTurn && candidateMove == null -> listOf(Analyze, Pass, endGameButton, chatButton, nextGameButton) isMyTurn && candidateMove != null -> listOf(ConfirmMove, DiscardMove) @@ -746,7 +751,19 @@ class GameViewModel( CancelDialogDismiss -> cancelDialogShowing = false ChatDialogDismiss -> chatDialogShowing = false KOMoveDialogDismiss -> koMoveDialogShowing = false - is ChatSend -> gameConnection.sendMessage(action.message, gameState?.moves?.size ?: 0) + is ChatSend -> gameConnection.sendMessage(action.message, gameState?.moves?.size ?: 0, when (action.channel) { + ChatChannel.MAIN -> "main" + ChatChannel.MALKOVICH -> "malkovich" + ChatChannel.SPECTATOR -> "spectator" + ChatChannel.PERSONAL -> "personal" + }) + is VariationSend -> { + gameConnection.sendAnalysisMessage(AnalysisMessage( + name = action.title, + from = currentVariation!!.rootMoveNo, + moves = currentVariation!!.moves, + ), gameState?.moves?.size ?: 0, "main") + } is OpenVariation -> { chatDialogShowing = false analyzeMode = true @@ -1034,7 +1051,8 @@ sealed interface UserAction { object PlayerDetailsDialogDismissed: UserAction object ChatDialogDismiss: UserAction object KOMoveDialogDismiss: UserAction - class ChatSend(val message: String): UserAction + class ChatSend(val message: String, val channel: ChatChannel): UserAction + class VariationSend(val title: String): UserAction class OpenVariation(val variation: Variation): UserAction object OpenInBrowser: UserAction object DownloadSGF: UserAction diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/composables/ChatDialog.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/composables/ChatDialog.kt index dfa2302e..c9da4347 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/composables/ChatDialog.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/game/composables/ChatDialog.kt @@ -3,6 +3,7 @@ package io.zenandroid.onlinego.ui.screens.game.composables import android.content.Intent import android.net.Uri import androidx.activity.compose.BackHandler +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -12,6 +13,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.SelectionContainer @@ -19,17 +21,27 @@ import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material.* +import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Send import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorProducer import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.Placeholder @@ -40,8 +52,13 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.toSize +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import io.zenandroid.onlinego.data.model.ogs.ChatChannel import io.zenandroid.onlinego.data.model.BoardTheme import io.zenandroid.onlinego.data.model.Cell import io.zenandroid.onlinego.data.model.Position @@ -59,8 +76,10 @@ import io.zenandroid.onlinego.ui.theme.OnlineGoTheme fun ChatDialog( messages: Map>, game: Position, + inAnalysisMode: Boolean, onDialogDismiss: (() -> Unit), - onSendMessage: ((String) -> Unit), + onSendMessage: ((String, ChatChannel) -> Unit), + onSendVariation: ((String) -> Unit), onVariation: ((Variation) -> Unit), ) { BackHandler { onDialogDismiss() } @@ -168,7 +187,7 @@ fun ChatDialog( ({ onVariation(Variation( - rootMoveNo = it.message.variation!!.from ?: 0, + rootMoveNo = it.message.variation.from ?: 0, moves = it.message.variation.moves.orEmpty(), )) }) @@ -234,17 +253,41 @@ fun ChatDialog( } Row { var message by rememberSaveable { mutableStateOf("") } + var channelIndex by rememberSaveable { mutableStateOf(0) } TextField( value = message, onValueChange = { message = it }, colors = TextFieldDefaults.textFieldColors( textColor = MaterialTheme.colors.onSurface ), - modifier = Modifier.weight(1f), + modifier = Modifier + .background( + color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.BackgroundOpacity), + shape = MaterialTheme.shapes.small.copy(topEnd = ZeroCornerSize, bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize) + ) + .weight(1f), ) + ExposedDropDownMenu( + values = if (inAnalysisMode) + listOf("Chat", "Malkovich", "Variation") + else + listOf("Chat", "Malkovich"), + selectedIndex = channelIndex, + onChange = { index -> channelIndex = index }, + modifier = Modifier + ) { + Text( + text = "Mode", + modifier = Modifier + ) + } IconButton( onClick = { - onSendMessage(message) + when (channelIndex) { + 0 -> onSendMessage(message, ChatChannel.MAIN) + 1 -> onSendMessage(message, ChatChannel.MALKOVICH) + 2 -> onSendVariation(message) + } message = "" }, enabled = message.isNotBlank() @@ -260,6 +303,106 @@ fun ChatDialog( } } +@Composable +private fun ExposedDropDownMenu( + values: List, + selectedIndex: Int, + onChange: (Int) -> Unit, + modifier: Modifier = Modifier, + backgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.BackgroundOpacity), + shape: Shape = MaterialTheme.shapes.small.copy(topStart = ZeroCornerSize, bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize), + label: @Composable () -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + var textfieldSize by remember { mutableStateOf(Size.Zero) } + + val indicatorColor = + if (expanded) MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high) + else MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.UnfocusedIndicatorLineOpacity) + val indicatorWidth = (if (expanded) 2 else 1).dp + val labelColor = + if (expanded) MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high) + else MaterialTheme.colors.onSurface.copy(ContentAlpha.medium) + val trailingIconColor = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.IconOpacity) + + val rotation: Float by animateFloatAsState(if (expanded) 180f else 0f) + + val focusManager = LocalFocusManager.current + + Column(modifier = modifier.width(IntrinsicSize.Min)) { + Box( + Modifier + .drawBehind { + val strokeWidth = indicatorWidth.value * density + val y = size.height - strokeWidth / 2 + drawLine( + indicatorColor, + Offset(0f, y), + Offset(size.width, y), + strokeWidth + ) + } + ) { + Box( + Modifier.height(56.dp) // TODO: responsive + .fillMaxWidth() + .background(color = backgroundColor, shape = shape) + .onGloballyPositioned { textfieldSize = it.size.toSize() } + .clip(shape) + .clickable { + expanded = !expanded + focusManager.clearFocus() + } + .padding(start = 16.dp, end = 12.dp, top = 7.dp, bottom = 10.dp) + ) { + Column(Modifier.padding(end = 32.dp)) { + ProvideTextStyle(value = MaterialTheme.typography.caption.copy(color = labelColor)) { + label() + } + Text( + text = values[selectedIndex], + modifier = Modifier.padding(top = 1.dp) + ) + } + Icon( + imageVector = Icons.Filled.ArrowDropDown, + contentDescription = "Change", + tint = trailingIconColor, + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(top = 4.dp) + .rotate(rotation) + ) + + } + } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier + .widthIn(with(LocalDensity.current) { textfieldSize.width.toDp() }) + ) { + values.forEachIndexed { i, v -> + val scope = rememberCoroutineScope() + DropdownMenuItem( + onClick = { + onChange(i) + scope.launch { + delay(150) + expanded = false + } + } + ) { + Text( + text = v, + ) + } + } + } + } +} + @Preview(showBackground = true) @Composable fun Preview() { @@ -277,8 +420,10 @@ fun Preview() { ) ), game = Position(9,9), + inAnalysisMode = true, onDialogDismiss = {}, - onSendMessage = {}, + onSendMessage = { _, _ -> }, + onSendVariation = {}, onVariation = {} ) }