This commit is contained in:
Eric85710 2026-06-29 14:55:32 +08:00
parent a2fc422519
commit 1d92471ba1
3 changed files with 75 additions and 13 deletions

View file

@ -1,5 +1,6 @@
package com.example.login_v3.data.di
import android.util.Log
import okhttp3.*
import javax.inject.Inject
import javax.inject.Singleton
@ -8,44 +9,82 @@ import kotlinx.coroutines.flow.SharedFlow
@Singleton
class ChatWebSocketManager @Inject constructor(
private val client: OkHttpClient // 可以從 Hilt 模組注入
private val client: OkHttpClient // 從 NetworkModule 注入
) {
private var webSocket: WebSocket? = null
private var currentUrl: String? = null // 🎯 新增:用來記錄當前的連線網址
// 用 SharedFlow 把收到的字串推出去給 Repository 監聽
private val _incomingMessages = MutableSharedFlow<String>(extraBufferCapacity = 64)
val incomingMessages: SharedFlow<String> = _incomingMessages
fun connect(url: String) {
if (webSocket != null) return // 避免重複連線
// 🎯 核心修正:檢查是否已經有全域連線存在
// 我們去掉網址後面的 ?token=... 參數,只比對前半段的 Base URL (wss://tg.technologia-tw.com/api/ws)
val newBaseUrl = url.split("?").firstOrNull()
val currentBaseUrl = currentUrl?.split("?")?.firstOrNull()
if (webSocket != null && newBaseUrl == currentBaseUrl) {
Log.d("ChatWebSocketManager", "全域 WebSocket 已在連線中,跳過重複連線。")
return
}
// 如果連線完全不存在,或者域名變了,才需要建立新連線(安全起見先清理舊連線)
if (webSocket != null) {
disconnect()
}
currentUrl = url
val request = Request.Builder().url(url).build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.d("ChatWebSocketManager", "WebSocket 連線成功!")
}
override fun onMessage(webSocket: WebSocket, text: String) {
// 收到伺服器即時訊息,丟進 Flow
Log.d("ChatWebSocketManager", "收到 Socket 訊息: $text")
_incomingMessages.tryEmit(text)
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
this@ChatWebSocketManager.webSocket = null
// 這裡可以寫自動重連邏輯
Log.d("ChatWebSocketManager", "WebSocket 已關閉: $reason")
clearConnection()
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
this@ChatWebSocketManager.webSocket = null
// 處理連線失敗
Log.e("ChatWebSocketManager", "WebSocket 連線失敗: ${t.message}, Code: ${response?.code}")
clearConnection()
// 💡 這裡未來可以實作計時器自動重連 (例如 delay 5秒後重新呼叫 connect)
}
})
}
// 進入房間時,通知後端我們要訂閱這個 Room 的即時訊息(看後端協議決定要不要傳這個)
// 進入房間時,通知後端我們要訂閱這個 Room 的即時訊息
fun subscribeToRoom(roomId: String) {
// 例如送出 JSON: { "action": "subscribe", "roomId": "xxx" }
webSocket?.send("{\"action\":\"subscribe\",\"roomId\":\"$roomId\"}")
// 💡 提示:請根據你與後端約定的 JSON 格式去修改這一行。
// 這裡假設後端接收的格式是 {"action":"subscribe","roomId":"xxxx"}
val subscribeJson = "{\"action\":\"subscribe\",\"roomId\":\"$roomId\"}"
val isSent = webSocket?.send(subscribeJson) ?: false
if (isSent) {
Log.d("ChatWebSocketManager", "已成功發送訂閱房間訊息: $roomId")
} else {
Log.e("ChatWebSocketManager", "發送訂閱房間失敗,Socket 可能未連線")
}
}
fun disconnect() {
// 1000 代表正常關閉
webSocket?.close(1000, "User exit")
clearConnection()
}
// 🎯 提取出來的清理專用方法
private fun clearConnection() {
webSocket = null
currentUrl = null
}
}

View file

@ -16,6 +16,7 @@ import com.example.login_v3.data.api.api_class.MessageResponse
import com.example.login_v3.data.api.api_class.RoomListResponse
import com.example.login_v3.data.api.api_class.SendMessageRequest
import com.example.login_v3.data.di.ChatWebSocketManager
import com.example.login_v3.data.repository.basic.TokenManager
import com.example.login_v3.home.Message.ViewModel.Detail.MessageDao
import com.example.login_v3.home.Message.ViewModel.Detail.MessageEntity
import com.example.login_v3.home.Message.ViewModel.Detail.MessageStatus
@ -25,6 +26,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@ -50,6 +52,7 @@ class ChatRoomsRepository @Inject constructor(
private val messageDao: MessageDao,
private val webSocketManager: ChatWebSocketManager, // 👈 1. 注入 Socket 管理器
private val moshi: Moshi, // 👈 用於解析 Socket 傳來的 JSON
private val tokenManager: TokenManager,
@ApplicationContext private val context: Context // 注入 ApplicationContext 用於處理 Uri 檔案
) {
@ -94,12 +97,26 @@ class ChatRoomsRepository @Inject constructor(
}
}
// 👈 4. 新增:提供給 ViewModel 在進入聊天室時呼叫的啟動連線功能
fun startChatSession(roomId: String) {
val webSocketUrl = "wss://tg.technologia-tw.com/api/ws?token=\$jwt"
// 🎯 1. 改成 suspend fun,以便讀取 tokenManager
suspend fun startChatSession(roomId: String) {
// 2. 從你的 tokenManager 撈出最新的 Access Token (與你原本在 OkHttp 裡撈取的方式一致)
val token = tokenManager.currentAccessToken.first() ?: ""
// 2. 啟動連線
if (token.isBlank()) {
Log.e("Repository", "WebSocket 連線失敗:找不到 Token")
return
}
// 3. 🎯 動態組裝後端指定的網址,把 $jwt 替換成真正的 Token
val webSocketUrl = "wss://tg.technologia-tw.com/api/ws?token=$token"
// 4. 啟動連線
webSocketManager.connect(webSocketUrl)
// 5. 💡 重要核心:因為是全域連線,後端怎麼知道你現在在看哪一個房間?
// 通常連線成功後,需要發送一個「訂閱房間」的 JSON 告訴後端:「Eric 現在進入了 roomId 聊天室,請把這個房間的即時訊息丟給我」
// 如果你們後端的設計是「只要連上,全域所有房間的訊息都會無腦推下來」,那下面這行就可以註解掉。
webSocketManager.subscribeToRoom(roomId)
}
suspend fun fetchRooms(): Result<RoomListResponse> {

View file

@ -123,6 +123,12 @@ class ChatViewModel @Inject constructor(
messageListenerJob = viewModelScope.launch {
// 🌟 優化防閃爍:只有在原本不是 Success 的情況下,才去 show 轉圈圈
try {
repository.startChatSession(roomId)
} catch (e: Exception) {
Log.e("ChatViewModel", "WebSocket 啟動失敗", e)
}
if (_uiState.value !is MessagesUiState.Success) {
_uiState.value = MessagesUiState.Loading
}