flash chat button on new message

This commit is contained in:
Md. Touhidur Rahman 2025-09-12 00:51:09 +06:00
parent 530873c6f5
commit a822fda6ac
No known key found for this signature in database
GPG Key ID: 431978882FE25058
5 changed files with 108 additions and 19 deletions

View File

@ -1,7 +1,9 @@
package com.unciv.logic.multiplayer.chat
import com.badlogic.gdx.Gdx
import com.unciv.UncivGame
import com.unciv.ui.screens.worldscreen.chat.ChatPopup
import com.unciv.utils.toUUIDOrNull
import java.util.Collections.synchronizedMap
import java.util.LinkedList
import java.util.Queue
@ -10,6 +12,8 @@ import java.util.UUID
data class Chat(
val gameId: UUID,
) {
var read = true
// <civName, message> pairs
private val messages: MutableList<Pair<String, String>> = mutableListOf(INITIAL_MESSAGE)
@ -52,6 +56,8 @@ object ChatStore {
*/
var chatPopup: ChatPopup? = null
var hasGlobalMessage = false
private var gameIdToChat: MutableMap<UUID, Chat> = synchronizedMap(mutableMapOf())
/** When no [ChatPopup] is open to receive these oddities, we keep them here.
@ -72,21 +78,34 @@ object ChatStore {
globalMessages = LinkedList()
}
fun relayChatMessage(chat: Response.Chat) {
fun relayChatMessage(incomingChatMsg: Response.Chat) {
Gdx.app.postRunnable {
if (chat.gameId == null || chat.gameId.isBlank()) {
relayGlobalMessage(chat.message, chat.civName)
if (incomingChatMsg.gameId == null || incomingChatMsg.gameId.isBlank()) {
relayGlobalMessage(incomingChatMsg.message, incomingChatMsg.civName)
} else {
val gameId = try {
UUID.fromString(chat.gameId)
UUID.fromString(incomingChatMsg.gameId)
} catch (_: Throwable) {
// Discard messages with invalid UUID
return@postRunnable
}
getChatByGameId(gameId).addMessage(chat.civName, chat.message)
if (chatPopup?.chat?.gameId == gameId) {
chatPopup?.addMessage(chat.civName, chat.message)
val chat = (chatPopup?.chat ?: getChatByGameId(gameId))
chat.addMessage(incomingChatMsg.civName, incomingChatMsg.message)
if (gameId.equals(chatPopup?.chat?.gameId)) {
chatPopup?.addMessage(incomingChatMsg.civName, incomingChatMsg.message)
}
if (chatPopup == null && incomingChatMsg.civName != "System") {
if (gameId.equals(UncivGame.Current.worldScreen?.gameInfo?.gameId?.toUUIDOrNull())) {
if (UncivGame.Current.worldScreen?.gameInfo?.currentPlayer != incomingChatMsg.civName) {
UncivGame.Current.worldScreen?.chatButton?.chat?.read = false
UncivGame.Current.worldScreen?.chatButton?.startFlashing()
}
} else {
// some other game not currently on screen has a message
chat.read = false
}
}
}
}
@ -103,7 +122,17 @@ object ChatStore {
fun relayGlobalMessage(message: String, civName: String = "System") {
Gdx.app.postRunnable {
chatPopup?.addMessage(civName, message, suffix = "one time") ?: globalMessages.add(Pair(civName, message))
if (civName != "System") {
hasGlobalMessage = chatPopup == null
if (hasGlobalMessage) UncivGame.Current.worldScreen?.chatButton?.startFlashing()
}
chatPopup?.addMessage(civName, message, suffix = "one time") ?: globalMessages.add(
Pair(
civName,
message
)
)
}
}
}

View File

@ -7,8 +7,8 @@ import com.badlogic.gdx.scenes.scene2d.ui.Cell
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.ui.components.fonts.Fonts
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.fonts.Fonts
import com.unciv.ui.screens.basescreen.BaseScreen
/**
@ -23,10 +23,11 @@ open class IconTextButton(
text: String,
val icon: Actor? = null,
fontSize: Int = Constants.defaultFontSize,
fontColor: Color = Color.WHITE
): Button(BaseScreen.skin) {
val fontColor: Color = Color.WHITE
) : Button(BaseScreen.skin) {
/** [Label] instance produced by, and with content and formatting as specified in [String.toLabel]. */
val label = text.toLabel(fontColor, fontSize, hideIcons = true) // Since by definition we already have an icon
/** Table cell containing the [icon] if any, or `null` (that is, when no [icon] was supplied, the Cell will exist but have no Actor). */
val iconCell: Cell<Actor> =
if (icon != null) {
@ -37,6 +38,7 @@ open class IconTextButton(
} else {
add().padRight(fontSize / 2f)
}
/** Table cell instance containing the [label]. */
val labelCell: Cell<Label> = add(label)

View File

@ -1,22 +1,38 @@
package com.unciv.ui.screens.worldscreen.chat
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.utils.Disposable
import com.unciv.UncivGame
import com.unciv.logic.multiplayer.chat.ChatStore
import com.unciv.logic.multiplayer.chat.ChatWebSocket
import com.unciv.logic.multiplayer.chat.Message
import com.unciv.ui.components.input.onClick
import com.unciv.ui.images.IconTextButton
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.worldscreen.WorldScreen
import com.unciv.utils.Concurrency
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
class ChatButton(val worldScreen: WorldScreen) : IconTextButton(
"Chat", ImageGetter.getImage("OtherIcons/Chat"), 23
) {
), Disposable {
val chat = ChatStore.getChatByGameId(worldScreen.gameInfo.gameId)
init {
width = 95f
iconCell.pad(3f).center()
if (!chat.read || ChatStore.hasGlobalMessage) {
startFlashing()
}
onClick {
ChatPopup(worldScreen).open()
stopFlashing()
ChatPopup(chat, worldScreen).open()
}
refreshVisibility()
@ -45,4 +61,38 @@ class ChatButton(val worldScreen: WorldScreen) : IconTextButton(
worldScreen.techPolicyAndDiplomacy.x.coerceAtLeast(1f),
worldScreen.techPolicyAndDiplomacy.y - height - 1f
)
private var flashJob: Job? = null
fun startFlashing(targetColor: Color = Color.ORANGE, interval: Duration = 500.milliseconds) {
flashJob?.cancel()
flashJob = Concurrency.run("ChatButton color flash") {
var isAlternatingColor = true
while (true) {
Gdx.app.postRunnable {
if (isAlternatingColor) {
icon?.color = targetColor
label.color = targetColor
} else {
icon?.color = fontColor
label.color = fontColor
}
isAlternatingColor = !isAlternatingColor
}
delay(interval)
}
}
}
private fun stopFlashing() {
flashJob?.cancel()
icon?.color = fontColor
label.color = fontColor
}
override fun dispose() {
flashJob?.cancel()
}
}

View File

@ -11,6 +11,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame
import com.unciv.logic.multiplayer.chat.Chat
import com.unciv.logic.multiplayer.chat.ChatStore
import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.coerceLightnessAtLeast
@ -28,6 +29,7 @@ private val civChatColorsMap = mapOf<String, Color>(
)
class ChatPopup(
val chat: Chat,
private val worldScreen: WorldScreen,
) : Popup(screen = worldScreen, scrollable = Scrollability.None) {
companion object {
@ -35,8 +37,6 @@ class ChatPopup(
const val CIVNAME_COLOR_MIN_LIGHTNESS = 0.60f
}
val chat = ChatStore.getChatByGameId(worldScreen.gameInfo.gameId)
private val chatTable = Table(skin)
private val scrollPane = ScrollPane(chatTable, skin)
private val messageField = UncivTextField(
@ -44,7 +44,9 @@ class ChatPopup(
)
init {
chat.read = true
ChatStore.chatPopup = this
ChatStore.hasGlobalMessage = false
chatTable.defaults().growX().pad(5f).center()
/**

View File

@ -4,12 +4,18 @@ import yairm210.purity.annotations.Pure
import java.util.UUID
/**
* Checks if a [String] is a valid UUID
* Tries to convert a [String] to a valid [UUID]
* and returns `null` upon failure
*/
@Pure
fun String.isUUID(): Boolean = try {
fun String.toUUIDOrNull(): UUID? = try {
UUID.fromString(this)
true
} catch (_: Throwable) {
false
null
}
/**
* Checks if a [String] is a valid [UUID]
*/
@Pure
fun String.isUUID(): Boolean = toUUIDOrNull() != null