diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 69acef90a..7f18d5a84 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,7 +29,7 @@ android { minSdk = 16 targetSdk = 35 versionCode = 121 - versionName = "2.40.1" + versionName = "2.40.2" } buildTypes { diff --git a/app/src/main/java/com/github/andreyasadchy/xtra/api/MiscApi.kt b/app/src/main/java/com/github/andreyasadchy/xtra/api/MiscApi.kt index 7c7d9070b..a53a3d575 100644 --- a/app/src/main/java/com/github/andreyasadchy/xtra/api/MiscApi.kt +++ b/app/src/main/java/com/github/andreyasadchy/xtra/api/MiscApi.kt @@ -36,6 +36,12 @@ interface MiscApi { @GET("https://7tv.io/v3/users/twitch/{channelId}") suspend fun getStvEmotes(@Path("channelId") channelId: String): StvChannelResponse + @GET("https://7tv.io/v3/users/twitch/{userId}") + suspend fun getStvUser(@Path("userId") userId: String): ResponseBody + + @POST("https://7tv.io/v3/users/{userId}/presences") + suspend fun sendStvPresence(@Path("userId") userId: String, @Body body: RequestBody) + @GET("https://api.betterttv.net/3/cached/emotes/global") suspend fun getGlobalBttvEmotes(): List diff --git a/app/src/main/java/com/github/andreyasadchy/xtra/repository/PlayerRepository.kt b/app/src/main/java/com/github/andreyasadchy/xtra/repository/PlayerRepository.kt index 9038f4ef8..664d984b1 100644 --- a/app/src/main/java/com/github/andreyasadchy/xtra/repository/PlayerRepository.kt +++ b/app/src/main/java/com/github/andreyasadchy/xtra/repository/PlayerRepository.kt @@ -21,13 +21,17 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject import okhttp3.Credentials import okhttp3.MediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okhttp3.ResponseBody +import org.json.JSONObject import retrofit2.Response import java.net.InetSocketAddress import java.net.Proxy @@ -276,6 +280,23 @@ class PlayerRepository @Inject constructor( } } + suspend fun getStvUser(userId: String): String? = withContext(Dispatchers.IO) { + JSONObject(misc.getStvUser(userId).string()).optJSONObject("user")?.optString("id") + } + + suspend fun sendStvPresence(stvUserId: String, channelId: String, sessionId: String?, self: Boolean) = withContext(Dispatchers.IO) { + val json = buildJsonObject { + put("kind", 1) + put("passive", self) + put("session_id", if (self) sessionId else "undefined") + putJsonObject("data") { + put("platform", "TWITCH") + put("id", channelId) + } + }.toString() + misc.sendStvPresence(stvUserId, RequestBody.create(MediaType.get("application/x-www-form-urlencoded; charset=utf-8"), json)) + } + suspend fun loadGlobalBttvEmotes(): List = withContext(Dispatchers.IO) { parseBttvEmotes(misc.getGlobalBttvEmotes()) } diff --git a/app/src/main/java/com/github/andreyasadchy/xtra/ui/chat/ChatFragment.kt b/app/src/main/java/com/github/andreyasadchy/xtra/ui/chat/ChatFragment.kt index 716b06aa3..d1cf6f130 100644 --- a/app/src/main/java/com/github/andreyasadchy/xtra/ui/chat/ChatFragment.kt +++ b/app/src/main/java/com/github/andreyasadchy/xtra/ui/chat/ChatFragment.kt @@ -333,7 +333,7 @@ class ChatFragment : BaseNetworkFragment(), LifecycleListener, MessageClickedDia val useChatWebSocket = requireContext().prefs().getBoolean(C.CHAT_USE_WEBSOCKET, false) val useSSL = requireContext().prefs().getBoolean(C.CHAT_USE_SSL, true) val usePubSub = requireContext().prefs().getBoolean(C.CHAT_PUBSUB_ENABLED, true) - val showNamePaints = requireContext().prefs().getBoolean(C.CHAT_SHOW_PAINTS, false) + val showNamePaints = requireContext().prefs().getBoolean(C.CHAT_SHOW_PAINTS, true) val emoteQuality = requireContext().prefs().getString(C.CHAT_IMAGE_QUALITY, "4") ?: "4" val animateGifs = requireContext().prefs().getBoolean(C.ANIMATED_EMOTES, true) val showUserNotice = requireContext().prefs().getBoolean(C.CHAT_SHOW_USERNOTICE, true) diff --git a/app/src/main/java/com/github/andreyasadchy/xtra/ui/chat/ChatViewModel.kt b/app/src/main/java/com/github/andreyasadchy/xtra/ui/chat/ChatViewModel.kt index 0cc80f36b..6aaef291a 100644 --- a/app/src/main/java/com/github/andreyasadchy/xtra/ui/chat/ChatViewModel.kt +++ b/app/src/main/java/com/github/andreyasadchy/xtra/ui/chat/ChatViewModel.kt @@ -519,6 +519,8 @@ class ChatViewModel @Inject constructor( private var eventSub: EventSubWebSocket? = null private var pubSub: PubSubWebSocket? = null private var stvEventApi: STVEventApiWebSocket? = null + private var stvUserId: String? = null + private var stvLastPresenceUpdate: Long? = null private val allEmotes = mutableListOf() private var usedRaidId: String? = null @@ -588,6 +590,15 @@ class ChatViewModel @Inject constructor( } if (showNamePaints && !channelId.isNullOrBlank()) { stvEventApi = STVEventApiWebSocket(channelId, okHttpClient, viewModelScope, this).apply { connect() } + if (isLoggedIn && !account.id.isNullOrBlank()) { + viewModelScope.launch { + try { + stvUserId = playerRepository.getStvUser(account.id).takeIf { !it.isNullOrBlank() } + } catch (e: Exception) { + + } + } + } } } @@ -605,6 +616,9 @@ class ChatViewModel @Inject constructor( private fun onChatMessage(message: ChatMessage) { onMessage(message) addChatter(message.userName) + if (isLoggedIn && !account.id.isNullOrBlank() && message.userId == account.id) { + onUpdatePresence(null, false) + } } override fun onConnect() { @@ -934,9 +948,28 @@ class ChatViewModel @Inject constructor( } override fun onUserUpdate(userId: String, paintId: String) { - paintUsers.entries.find { it.key == userId }?.let { paintUsers.remove(it.key) } - paintUsers.put(userId, paintId) - newPaintUser.value = Pair(userId, paintId) + val item = paintUsers.entries.find { it.key == userId } + if (item == null || item.value != paintId) { + item?.let { paintUsers.remove(it.key) } + paintUsers.put(userId, paintId) + newPaintUser.value = Pair(userId, paintId) + } + } + + override fun onUpdatePresence(sessionId: String?, self: Boolean) { + stvUserId?.let { stvUserId -> + if (stvUserId.isNotBlank() && !channelId.isNullOrBlank() && (self && !sessionId.isNullOrBlank() || !self) && + stvLastPresenceUpdate?.let { (System.currentTimeMillis() - it) > 10000 } != false) { + stvLastPresenceUpdate = System.currentTimeMillis() + viewModelScope.launch { + try { + playerRepository.sendStvPresence(stvUserId, channelId, sessionId, self) + } catch (e: Exception) { + + } + } + } + } } fun addEmotes(list: List) { diff --git a/app/src/main/java/com/github/andreyasadchy/xtra/ui/view/chat/ChatView.kt b/app/src/main/java/com/github/andreyasadchy/xtra/ui/view/chat/ChatView.kt index 0591446cd..b54a3f4cb 100644 --- a/app/src/main/java/com/github/andreyasadchy/xtra/ui/view/chat/ChatView.kt +++ b/app/src/main/java/com/github/andreyasadchy/xtra/ui/view/chat/ChatView.kt @@ -126,7 +126,7 @@ class ChatView : ConstraintLayout { isLightTheme = context.isLightTheme, nameDisplay = context.prefs().getString(C.UI_NAME_DISPLAY, "0"), useBoldNames = context.prefs().getBoolean(C.CHAT_BOLDNAMES, false), - showNamePaints = context.prefs().getBoolean(C.CHAT_SHOW_PAINTS, false), + showNamePaints = context.prefs().getBoolean(C.CHAT_SHOW_PAINTS, true), namePaintsList = namePaints, paintUsersMap = paintUsers, showSystemMessageEmotes = context.prefs().getBoolean(C.CHAT_SYSTEM_MESSAGE_EMOTES, true), @@ -438,14 +438,23 @@ class ChatView : ConstraintLayout { paintUsers.entries.find { it.key == pair.first }?.let { paintUsers.remove(it.key) } paintUsers.put(pair.first, pair.second) } + adapter.messages?.toList()?.let { messages -> + messages.filter { it.userId == pair.first }.forEach { message -> + messages.indexOf(message).takeIf { it != -1 }?.let { + adapter.notifyItemChanged(it) + } + } + } messageDialog?.adapter?.paintUsers?.let { paintUsers -> paintUsers.entries.find { it.key == pair.first }?.let { paintUsers.remove(it.key) } paintUsers.put(pair.first, pair.second) } + messageDialog?.updatePaint(pair.first) replyDialog?.adapter?.paintUsers?.let { paintUsers -> paintUsers.entries.find { it.key == pair.first }?.let { paintUsers.remove(it.key) } paintUsers.put(pair.first, pair.second) } + replyDialog?.updatePaint(pair.first) } fun setUsername(username: String?) { diff --git a/app/src/main/java/com/github/andreyasadchy/xtra/ui/view/chat/MessageClickedDialog.kt b/app/src/main/java/com/github/andreyasadchy/xtra/ui/view/chat/MessageClickedDialog.kt index ed168f357..bf9b2a2c8 100644 --- a/app/src/main/java/com/github/andreyasadchy/xtra/ui/view/chat/MessageClickedDialog.kt +++ b/app/src/main/java/com/github/andreyasadchy/xtra/ui/view/chat/MessageClickedDialog.kt @@ -293,6 +293,18 @@ class MessageClickedDialog : BottomSheetDialogFragment(), IntegrityDialog.Callba } } + fun updatePaint(userId: String) { + adapter?.let { adapter -> + adapter.messages?.toList()?.let { messages -> + messages.filter { it.userId == userId }.forEach { message -> + messages.indexOf(message).takeIf { it != -1 }?.let { + adapter.notifyItemChanged(it) + } + } + } + } + } + fun scrollToLastPosition() { if (!isChatTouched && !shouldShowButton()) { adapter?.messages?.let { binding.recyclerView.scrollToPosition(it.lastIndex) } diff --git a/app/src/main/java/com/github/andreyasadchy/xtra/ui/view/chat/ReplyClickedDialog.kt b/app/src/main/java/com/github/andreyasadchy/xtra/ui/view/chat/ReplyClickedDialog.kt index d465c5e0f..78939fd1d 100644 --- a/app/src/main/java/com/github/andreyasadchy/xtra/ui/view/chat/ReplyClickedDialog.kt +++ b/app/src/main/java/com/github/andreyasadchy/xtra/ui/view/chat/ReplyClickedDialog.kt @@ -161,6 +161,18 @@ class ReplyClickedDialog : BottomSheetDialogFragment() { } } + fun updatePaint(userId: String) { + adapter?.let { adapter -> + adapter.messages?.toList()?.let { messages -> + messages.filter { it.userId == userId }.forEach { message -> + messages.indexOf(message).takeIf { it != -1 }?.let { + adapter.notifyItemChanged(it) + } + } + } + } + } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/src/main/java/com/github/andreyasadchy/xtra/util/chat/STVEventApiListener.kt b/app/src/main/java/com/github/andreyasadchy/xtra/util/chat/STVEventApiListener.kt index a645a5c8e..83a826be3 100644 --- a/app/src/main/java/com/github/andreyasadchy/xtra/util/chat/STVEventApiListener.kt +++ b/app/src/main/java/com/github/andreyasadchy/xtra/util/chat/STVEventApiListener.kt @@ -5,4 +5,5 @@ import com.github.andreyasadchy.xtra.model.chat.NamePaint interface STVEventApiListener { fun onPaintUpdate(paint: NamePaint) fun onUserUpdate(userId: String, paintId: String) + fun onUpdatePresence(sessionId: String?, self: Boolean) } diff --git a/app/src/main/java/com/github/andreyasadchy/xtra/util/chat/STVEventApiWebSocket.kt b/app/src/main/java/com/github/andreyasadchy/xtra/util/chat/STVEventApiWebSocket.kt index 4fe9975e1..35e61fc17 100644 --- a/app/src/main/java/com/github/andreyasadchy/xtra/util/chat/STVEventApiWebSocket.kt +++ b/app/src/main/java/com/github/andreyasadchy/xtra/util/chat/STVEventApiWebSocket.kt @@ -175,6 +175,13 @@ class STVEventApiWebSocket( } } } + OPCODE_HELLO -> { + val data = json.optJSONObject("d") + val sessionId = data?.optString("session_id") + if (sessionId != null) { + listener.onUpdatePresence(sessionId, true) + } + } OPCODE_RECONNECT -> reconnect() } } catch (e: Exception) { @@ -189,6 +196,7 @@ class STVEventApiWebSocket( companion object { private const val OPCODE_DISPATCH = 0 + private const val OPCODE_HELLO = 1 private const val OPCODE_RECONNECT = 4 private const val OPCODE_SUBSCRIBE = 35 } diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index b3baf42e1..018a005e8 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -383,7 +383,7 @@ app:singleLineTitle="false" />