fix: 修正聊天室雙向即時同步失效問題

- echo.js:還原 disconnect()/connect(),確保 Reverb 重連後重新授權 presence channel
- BookingChat.vue:移除 handleNotificationFallback(會覆蓋樂觀更新訊息導致重複)
- BookingChat.vue:改用 state_change 事件偵測重連('reconnected' 並非合法 Pusher.js 事件)
- BookingChat.vue:移除未使用的 useAuthStore / useCoachAuthStore / currentUserId 依賴

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 18:29:53 +08:00
parent 6b3a0816f5
commit 5ba598c78f
2 changed files with 13 additions and 30 deletions
+10 -27
View File
@@ -4,8 +4,6 @@ import api from '../api/axios'
import coachApi from '../api/coachAxios' import coachApi from '../api/coachAxios'
import echo from '../plugins/echo' import echo from '../plugins/echo'
import { useNotificationStore } from '../stores/notifications' import { useNotificationStore } from '../stores/notifications'
import { useAuthStore } from '../stores/auth'
import { useCoachAuthStore } from '../stores/coachAuth'
const props = defineProps({ const props = defineProps({
bookingId: { type: Number, required: true }, bookingId: { type: Number, required: true },
@@ -23,12 +21,6 @@ const otherUserOnline = ref(false)
const channel = ref(null) const channel = ref(null)
const notificationStore = useNotificationStore() const notificationStore = useNotificationStore()
const authStore = useAuthStore()
const coachAuthStore = useCoachAuthStore()
const currentUserId = computed(() =>
props.currentUserType === 'provider' ? coachAuthStore.user?.id : authStore.user?.id
)
const isConfirmed = computed(() => props.bookingStatus === 'confirmed') const isConfirmed = computed(() => props.bookingStatus === 'confirmed')
const isCompleted = computed(() => props.bookingStatus === 'completed') const isCompleted = computed(() => props.bookingStatus === 'completed')
@@ -103,7 +95,7 @@ async function sendText() {
const tempId = `_temp_${Date.now()}` const tempId = `_temp_${Date.now()}`
messages.value.push({ messages.value.push({
id: tempId, id: tempId,
sender_id: currentUserId.value, sender_id: null,
sender_type: props.currentUserType, sender_type: props.currentUserType,
type: 'text', type: 'text',
content, content,
@@ -201,39 +193,30 @@ function subscribeChannel() {
}) })
} }
function handleReconnected() { // updateEchoToken() 會 disconnect/connectconnection 重建後需重訂 presence channel
// Pusher.js 的重連事件走 state_change,用 previous 判斷是否為重連而非初次連線
let everConnected = false
function onConnectionStateChange({ current }) {
if (current !== 'connected') return
if (!everConnected) { everConnected = true; return }
// 重連:重訂 presence channel
if (!isConfirmed.value) return if (!isConfirmed.value) return
echo.leave(`booking.${props.bookingId}`) echo.leave(`booking.${props.bookingId}`)
channel.value = null channel.value = null
subscribeChannel() subscribeChannel()
} }
// Fallbackpresence channel 失效時,收到 bell 通知也補拉訊息
async function handleNotificationFallback() {
const lastId = messages.value[messages.value.length - 1]?.id
if (lastId && typeof lastId === 'string') return // 還在樂觀更新中,不拉
await loadHistory()
}
onMounted(async () => { onMounted(async () => {
await requestBrowserNotificationPermission() await requestBrowserNotificationPermission()
await loadHistory() await loadHistory()
if (isConfirmed.value) { if (isConfirmed.value) {
subscribeChannel() subscribeChannel()
echo.connector.pusher.connection.bind('reconnected', handleReconnected) echo.connector.pusher.connection.bind('state_change', onConnectionStateChange)
if (currentUserId.value) {
echo.private(`App.Models.User.${currentUserId.value}`)
.listen('.notification.created', handleNotificationFallback)
}
} }
}) })
onUnmounted(() => { onUnmounted(() => {
echo.connector.pusher.connection.unbind('reconnected', handleReconnected) echo.connector.pusher.connection.unbind('state_change', onConnectionStateChange)
if (currentUserId.value) {
echo.private(`App.Models.User.${currentUserId.value}`)
.stopListening('.notification.created', handleNotificationFallback)
}
if (channel.value) { if (channel.value) {
channel.value.whisper('presence', { user_type: props.currentUserType, online: false }) channel.value.whisper('presence', { user_type: props.currentUserType, online: false })
echo.leave(`booking.${props.bookingId}`) echo.leave(`booking.${props.bookingId}`)
+3 -3
View File
@@ -24,16 +24,16 @@ const echo = new Echo({
}, },
}) })
// 登入後更新 auth header,讓 presence channel 授權帶正確 token // 登入後更新 auth header 並強制重連,確保 Reverb 用新 token 授權 channel
export function updateEchoToken() { export function updateEchoToken() {
const token = getAuthToken() const token = getAuthToken()
const bearer = `Bearer ${token}` const bearer = `Bearer ${token}`
echo.options.auth.headers.Authorization = bearer echo.options.auth.headers.Authorization = bearer
// 同步更新 Pusher 內部 config(避免 Pusher.js 持有舊的 header copy
if (echo.connector?.pusher?.config?.auth?.headers) { if (echo.connector?.pusher?.config?.auth?.headers) {
echo.connector.pusher.config.auth.headers.Authorization = bearer echo.connector.pusher.config.auth.headers.Authorization = bearer
} }
// 不 disconnect/connect:避免中斷 BookingChat 已訂閱的 presence channel echo.disconnect()
echo.connect()
} }
export default echo export default echo