diff --git a/app/src/main/java/com/example/login_v3/data/di/WebSocketManager.kt b/app/src/main/java/com/example/login_v3/data/di/WebSocketManager.kt index 72b365a..0f08e6d 100644 --- a/app/src/main/java/com/example/login_v3/data/di/WebSocketManager.kt +++ b/app/src/main/java/com/example/login_v3/data/di/WebSocketManager.kt @@ -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(extraBufferCapacity = 64) val incomingMessages: SharedFlow = _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 } } \ No newline at end of file diff --git a/app/src/main/java/com/example/login_v3/data/repository/dm/DM_Basic_Repository.kt b/app/src/main/java/com/example/login_v3/data/repository/dm/DM_Basic_Repository.kt index fa8e2b8..cbc2182 100644 --- a/app/src/main/java/com/example/login_v3/data/repository/dm/DM_Basic_Repository.kt +++ b/app/src/main/java/com/example/login_v3/data/repository/dm/DM_Basic_Repository.kt @@ -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 { diff --git a/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Message_Messaging_ViewModel.kt b/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Message_Messaging_ViewModel.kt index f78e8ef..160d042 100644 --- a/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Message_Messaging_ViewModel.kt +++ b/app/src/main/java/com/example/login_v3/home/Message/ViewModel/Detail/Message_Messaging_ViewModel.kt @@ -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 }