const app = getApp(); import { formatDate } from '../../static/lib/utils'; import { resolveAvatarUrl, getDefaultAvatarUrl } from '../../utils/avatar.js'; import { getLocalImUserId } from '../../utils/im-user.js'; Page({ data: { toUserId: '', toName: '', toAvatar: '', currentUser: null, messages: [], inputText: '', showEmojiPanel: false, showPlusPanel: false, showOrderSender: false, loadingHistory: false, isRefreshing: false, lastTimestamp: null, hasMore: true, scrollToView: '', detailText: '', showDetailModal: false, lastTapTime: 0, lastTapMsgId: '', emojiList: ['๐Ÿ˜€','๐Ÿ˜','๐Ÿ˜‚','๐Ÿคฃ','๐Ÿ˜ƒ','๐Ÿ˜„','๐Ÿ˜…','๐Ÿ˜†','๐Ÿ˜‰','๐Ÿ˜Š','๐Ÿ˜‹','๐Ÿ˜Ž','๐Ÿ˜','๐Ÿ˜˜','๐Ÿ˜—','๐Ÿ˜™','๐Ÿ˜š','๐Ÿ™‚','๐Ÿค—','๐Ÿคฉ','๐Ÿค”','๐Ÿคจ','๐Ÿ˜','๐Ÿ˜‘','๐Ÿ˜ถ','๐Ÿ™„','๐Ÿ˜','๐Ÿ˜ฃ','๐Ÿ˜ฅ','๐Ÿ˜ฎ','๐Ÿค','๐Ÿ˜ฏ','๐Ÿ˜ช','๐Ÿ˜ซ','๐Ÿ˜ด','๐Ÿ˜Œ','๐Ÿ˜›','๐Ÿ˜œ','๐Ÿ˜','๐Ÿคค','๐Ÿ˜’','๐Ÿ˜“','๐Ÿ˜”','๐Ÿ˜•','๐Ÿ™ƒ','๐Ÿค‘','๐Ÿ˜ฒ','โ˜น','๐Ÿ™','๐Ÿ˜–','๐Ÿ˜ž','๐Ÿ˜Ÿ','๐Ÿ˜ค','๐Ÿ˜ข','๐Ÿ˜ญ','๐Ÿ˜ฆ','๐Ÿ˜ง','๐Ÿ˜จ','๐Ÿ˜ฉ','๐Ÿคฏ','๐Ÿ˜ฌ','๐Ÿ˜ฐ','๐Ÿ˜ฑ','๐Ÿฅต','๐Ÿฅถ','๐Ÿ˜ณ','๐Ÿคช','๐Ÿ˜ต','๐Ÿ˜ก','๐Ÿ˜ ','๐Ÿคฌ','๐Ÿ˜ท','๐Ÿค’','๐Ÿค•','๐Ÿคข','๐Ÿคฎ','๐Ÿคง','๐Ÿ˜‡','๐Ÿค ','๐Ÿคก','๐Ÿคฅ','๐Ÿคซ','๐Ÿคญ','๐Ÿง','๐Ÿค“','๐Ÿ˜ˆ','๐Ÿ‘ฟ','๐Ÿ‘น','๐Ÿ‘บ','๐Ÿ’€','๐Ÿ‘ป','๐Ÿ‘ฝ','๐Ÿค–','๐Ÿ’ฉ','๐Ÿ˜บ','๐Ÿ˜ธ','๐Ÿ˜น','๐Ÿ˜ป','๐Ÿ˜ผ','๐Ÿ˜ฝ','๐Ÿ™€','๐Ÿ˜ฟ','๐Ÿ˜พ'], pendingImage: '', pendingImageFile: null, keyboardHeight: 0, bottomSafeHeight: 10 }, onLoad(options) { if (options.data) { try { const p = JSON.parse(decodeURIComponent(options.data)); this.setData({ toUserId: p.toUserId || '', toName: p.toName || '่Šๅคฉ', toAvatar: resolveAvatarUrl(p.toAvatar, app), }); } catch (e) {} } wx.setNavigationBarTitle({ title: this.data.toName }); this.initCurrentUser(); }, onShow() { this.ensureChatReady(); }, onHide() { this.clearPageListeners(); this.markPrivateMessageAsRead(); setTimeout(() => { if (app.loadConversations) app.loadConversations(); }, 250); }, ensureChatReady() { const targetUserId = getLocalImUserId(app); if (!targetUserId) return; const ready = () => { this.setupAllListeners(); this.loadHistory(true); }; if (app.ensureConnection) { const ret = app.ensureConnection(); if (ret && typeof ret.then === 'function') { ret.then(ready).catch(ready); } else { setTimeout(ready, 300); } return; } if (this.isConnected() && wx.goEasy.im?.userId === targetUserId) { ready(); return; } this.connectGoEasy(targetUserId); }, autoConnect() { this.ensureChatReady(); }, connectGoEasy(userId) { const { currentUser } = this.data; if (!currentUser) return; const defAvatar = resolveAvatarUrl('', app); wx.goEasy.connect({ id: userId, data: { name: currentUser.name, avatar: resolveAvatarUrl(currentUser.avatar, app) || defAvatar, }, onSuccess: () => { this.setupAllListeners(); this.loadHistory(true); }, onFailed: (error) => { if (error.code === 408) { this.setupAllListeners(); this.loadHistory(true); } else { wx.showToast({ title: '่ฟžๆŽฅๅคฑ่ดฅ๏ผŒ่ฏท้‡่ฏ•', icon: 'none' }); } } }); }, setupAllListeners() { this.clearPageListeners(); this.listenNewMsg(); this.listenRecall(); this.listenDelete(); this.listenRead(); }, clearPageListeners() { if (this._msgHandler) { wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, this._msgHandler); this._msgHandler = null; } if (this._recallHandler) { wx.goEasy.im.off(wx.GoEasy.IM_EVENT.MESSAGE_RECALLED, this._recallHandler); this._recallHandler = null; } if (this._deleteHandler) { wx.goEasy.im.off(wx.GoEasy.IM_EVENT.MESSAGE_DELETED, this._deleteHandler); this._deleteHandler = null; } if (this._readHandler) { wx.goEasy.im.off(wx.GoEasy.IM_EVENT.MESSAGE_READ, this._readHandler); this._readHandler = null; } }, clearAllListeners() { this.clearPageListeners(); }, initCurrentUser() { const imId = getLocalImUserId(app); const uid = wx.getStorageSync('uid'); const nick = app.globalData.nicheng || app.globalData.dashouNicheng || wx.getStorageSync('nicheng') || ''; this.setData({ currentUser: { id: imId || ('Ds' + uid), name: nick || `็”จๆˆท${uid}`, avatar: resolveAvatarUrl(wx.getStorageSync('touxiang'), app), }, }); }, fixAvatar(url) { return resolveAvatarUrl(url, app); }, buildImageFile(tempPath, size) { return { path: tempPath, tempFilePath: tempPath, size: size || 0, }; }, normalizeMessage(msg) { if (!msg) return msg; if (!msg.payload) msg.payload = {}; if (msg.type === 'image') { const p = msg.payload; if (!p.url) p.url = p.thumbnail || p.thumb || p.imageUrl || ''; } return msg; }, mergeSentMessage(localId, sent) { if (!sent) { this.updateMsg(localId, 'success'); return; } const msgs = this.data.messages.map((m) => { if (m.messageId !== localId) return m; return this.normalizeMessage({ ...m, ...sent, status: 'success', messageId: sent.messageId || m.messageId, payload: sent.payload || m.payload, senderData: m.senderData || sent.senderData, }); }); this.setData({ messages: msgs }); }, isConnected() { const s = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected'; return s === 'connected' || s === 'reconnected'; }, onPullDownRefresh() { if (this.data.loadingHistory) return; this.setData({ isRefreshing: true }); this.loadHistory(false).finally(() => { this.setData({ isRefreshing: false }); wx.stopPullDownRefresh(); }); }, loadHistory(refresh) { if (!refresh && !this.data.hasMore) return Promise.resolve(); return new Promise((resolve) => { this.setData({ loadingHistory: true }); const { toUserId, lastTimestamp } = this.data; const ts = refresh ? null : lastTimestamp; wx.goEasy.im.history({ type: wx.GoEasy.IM_SCENE.PRIVATE, id: toUserId, lastTimestamp: ts, limit: 20, onSuccess: (res) => { let list = res.content || []; list.sort((a, b) => a.timestamp - b.timestamp); list.forEach((m, idx) => { m = this.normalizeMessage(m); list[idx] = m; m.formattedTime = formatDate(m.timestamp); if (idx === 0) m.showTime = true; else m.showTime = (m.timestamp - list[idx-1].timestamp) / 60000 > 5; if (m.senderId === this.data.currentUser.id) { m.senderData = { name: this.data.currentUser.name, avatar: resolveAvatarUrl(this.data.currentUser.avatar, app), }; } else { m.senderData = { name: this.data.toName, avatar: resolveAvatarUrl(this.data.toAvatar, app), }; } }); let final = refresh ? list : [...list, ...this.data.messages]; if (!refresh) { for (let i=0; i 5; } } const ids = new Set(); const unique = []; for (const m of final) { if (!ids.has(m.messageId)) { ids.add(m.messageId); unique.push(m); } } const update = { messages: unique, hasMore: list.length >= 20, lastTimestamp: unique.length ? unique[0].timestamp : lastTimestamp, loadingHistory: false }; if (refresh) update.scrollToView = 'msg-bottom'; this.setData(update); if (refresh) this.markPrivateMessageAsRead(); resolve(); }, onFailed: () => { this.setData({ loadingHistory: false }); resolve(); } }); }); }, listenNewMsg() { if (this._msgHandler) wx.goEasy.im.off(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, this._msgHandler); this._msgHandler = (msg) => { const { currentUser, toUserId } = this.data; const relatedToMe = msg.senderId === currentUser.id || msg.receiverId === currentUser.id; const relatedToPeer = msg.senderId === toUserId || msg.receiverId === toUserId; if (!relatedToMe || !relatedToPeer) return; msg = this.normalizeMessage(msg); msg.formattedTime = formatDate(msg.timestamp); if (msg.senderId === currentUser.id) { msg.senderData = { name: currentUser.name, avatar: resolveAvatarUrl(currentUser.avatar, app), }; } else { msg.senderData = { name: this.data.toName, avatar: resolveAvatarUrl(this.data.toAvatar, app), }; } const msgs = this.data.messages; const last = msgs.length ? msgs[msgs.length-1] : null; msg.showTime = last ? (msg.timestamp - last.timestamp) / 60000 > 5 : true; this.setData({ messages: [...msgs, msg], scrollToView: 'msg-bottom' }); this.markPrivateMessageAsRead(); }; wx.goEasy.im.on(wx.GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, this._msgHandler); }, markPrivateMessageAsRead() { if (!this.data.toUserId) return; wx.goEasy.im.markMessageAsRead({ type: wx.GoEasy.IM_SCENE.PRIVATE, id: this.data.toUserId }); }, listenRead() { if (this._readHandler) wx.goEasy.im.off(wx.GoEasy.IM_EVENT.MESSAGE_READ, this._readHandler); this._readHandler = (msgs) => { if (!msgs || !msgs.length) return; const ids = new Set(msgs.map(m => m.messageId)); const newMsgs = this.data.messages.map(m => ids.has(m.messageId) ? { ...m, read: true } : m); this.setData({ messages: newMsgs }); }; wx.goEasy.im.on(wx.GoEasy.IM_EVENT.MESSAGE_READ, this._readHandler); }, listenRecall() { if (this._recallHandler) wx.goEasy.im.off(wx.GoEasy.IM_EVENT.MESSAGE_RECALLED, this._recallHandler); this._recallHandler = (msgs) => { if (!msgs || !msgs.length) return; const ids = new Set(msgs.map(m => m.messageId)); this.setData({ messages: this.data.messages.filter(m => !ids.has(m.messageId)) }); }; wx.goEasy.im.on(wx.GoEasy.IM_EVENT.MESSAGE_RECALLED, this._recallHandler); }, listenDelete() { if (this._deleteHandler) wx.goEasy.im.off(wx.GoEasy.IM_EVENT.MESSAGE_DELETED, this._deleteHandler); this._deleteHandler = (msgs) => { if (!msgs || !msgs.length) return; const ids = new Set(msgs.map(m => m.messageId)); this.setData({ messages: this.data.messages.filter(m => !ids.has(m.messageId)) }); }; wx.goEasy.im.on(wx.GoEasy.IM_EVENT.MESSAGE_DELETED, this._deleteHandler); }, onInput(e) { this.setData({ inputText: e.detail.value }); }, chooseImage() { const that = this; wx.chooseMedia({ count: 1, mediaType: ['image'], sizeType: ['compressed'], sourceType: ['album', 'camera'], success(res) { const file = (res.tempFiles && res.tempFiles[0]) || null; if (!file || !file.tempFilePath) return; that.setData({ pendingImage: file.tempFilePath, pendingImageFile: that.buildImageFile(file.tempFilePath, file.size), }); }, fail() { wx.showToast({ title: '้€‰ๆ‹ฉๅ›พ็‰‡ๅคฑ่ดฅ', icon: 'none' }); }, }); }, clearPendingImage() { this.setData({ pendingImage: '', pendingImageFile: null }); }, sendMessage() { if (this.data.pendingImage) { this.sendImageMsg(this.data.pendingImage, this.data.pendingImageFile); this.setData({ pendingImage: '', pendingImageFile: null }); return; } const text = this.data.inputText.trim(); if (!text) return; this.sendText(text); this.setData({ inputText: '' }); }, sendText(text) { const { currentUser, toUserId } = this.data; if (!toUserId || !currentUser) return; const avatar = resolveAvatarUrl(currentUser.avatar, app); const name = currentUser.name || '็”จๆˆท'; const id = 'local-'+Date.now(); const local = { messageId:id, type:'text', timestamp:Date.now(), senderId:currentUser.id, receiverId:toUserId, senderData: { name, avatar }, payload:{text}, status:'sending', read:false, formattedTime:formatDate(Date.now()), showTime:this.needShow(Date.now()) }; this.setData({ messages: [...this.data.messages, local], scrollToView: 'msg-bottom' }); const msg = wx.goEasy.im.createTextMessage({ text, to:{ type:wx.GoEasy.IM_SCENE.PRIVATE, id:toUserId, data:{ avatar, name } } }); wx.goEasy.im.sendMessage({ message:msg, onSuccess:() => this.updateMsg(id, 'success'), onFailed:() => this.updateMsg(id, 'failed') }); }, sendImageMsg(filePath, fileObj) { const { currentUser, toUserId } = this.data; if (!toUserId || !currentUser || !filePath) return; if (!wx.goEasy || !wx.goEasy.im) { wx.showToast({ title: '่Šๅคฉๆœช่ฟžๆŽฅ', icon: 'none' }); return; } const avatar = resolveAvatarUrl(currentUser.avatar, app); const name = currentUser.name || '็”จๆˆท'; const id = 'img-'+Date.now(); const imageFile = fileObj || this.buildImageFile(filePath, 0); const local = { messageId:id, type:'image', timestamp:Date.now(), senderId:currentUser.id, receiverId:toUserId, senderData: { name, avatar }, payload:{url:filePath}, status:'sending', read:false, formattedTime:formatDate(Date.now()), showTime:this.needShow(Date.now()) }; this.setData({ messages: [...this.data.messages, local], scrollToView: 'msg-bottom' }); const imgMsg = wx.goEasy.im.createImageMessage({ file: imageFile, to:{ type:wx.GoEasy.IM_SCENE.PRIVATE, id:toUserId, data:{ avatar, name } } }); wx.goEasy.im.sendMessage({ message: imgMsg, onSuccess: (sent) => this.mergeSentMessage(id, sent), onFailed: (error) => { console.error('[็ง่Š] ๅ›พ็‰‡ๅ‘้€ๅคฑ่ดฅ', error); this.updateMsg(id, 'failed'); wx.showToast({ title: 'ๅ›พ็‰‡ๅ‘้€ๅคฑ่ดฅ', icon: 'none' }); }, }); }, updateMsg(id, status) { this.setData({ messages: this.data.messages.map(m => m.messageId === id ? { ...m, status } : m) }); }, needShow(ts) { const ms = this.data.messages; if (!ms.length) return true; const last = ms[ms.length-1]; return (ts - last.timestamp) / 60000 > 5; }, onBubbleTap(e) { const msgId = e.currentTarget.dataset.messageid; const text = e.currentTarget.dataset.text; const now = Date.now(); if (msgId === this.data.lastTapMsgId && now - this.data.lastTapTime < 350) { this.setData({ detailText: text, showDetailModal: true, lastTapTime:0, lastTapMsgId:'' }); } else { this.data.lastTapTime = now; this.data.lastTapMsgId = msgId; } }, hideDetail() { this.setData({ showDetailModal: false }); }, copyDetail() { wx.setClipboardData({ data: this.data.detailText }); }, showAction(e) { const msgId = e.currentTarget.dataset.messageid; if (!msgId) return; const msg = this.data.messages.find(m => m.messageId === msgId); if (!msg || msg.messageId.startsWith('local-')) return; const itemList = ['ๅคๅˆถ', 'ๅˆ ้™ค']; if (msg.senderId === this.data.currentUser.id && (Date.now() - msg.timestamp < 120000)) { itemList.push('ๆ’คๅ›ž'); } wx.showActionSheet({ itemList, success: (res) => { if (itemList[res.tapIndex] === 'ๅคๅˆถ') { if (msg.type === 'text') wx.setClipboardData({ data: msg.payload.text }); } else if (itemList[res.tapIndex] === 'ๅˆ ้™ค') { this.deleteMsg(msg); } else if (itemList[res.tapIndex] === 'ๆ’คๅ›ž') { this.recallMsg(msg); } } }); }, deleteMsg(msg) { wx.showModal({ title: 'ๆ็คบ', content: 'ไป…่‡ชๅทฑ็œ‹ไธๅˆฐ', success: (res) => { if (res.confirm) { wx.goEasy.im.deleteMessage({ messages: [msg], onSuccess: () => { this.setData({ messages: this.data.messages.filter(m => m.messageId !== msg.messageId) }); }, onFailed: () => { this.setData({ messages: this.data.messages.filter(m => m.messageId !== msg.messageId) }); } }); } } }); }, recallMsg(msg) { wx.showModal({ title: 'ๆ็คบ', content: 'ๆ’คๅ›žๅŽๅฏนๆ–นไนŸ็œ‹ไธๅˆฐ', success: (res) => { if (res.confirm) { const newMsgs = this.data.messages.map(m => { if (m.messageId === msg.messageId) return { ...m, type:'text', payload:{text:'ไฝ ๆ’คๅ›žไบ†ไธ€ๆกๆถˆๆฏ'}, recalled:true }; return m; }); this.setData({ messages: newMsgs }); wx.goEasy.im.recallMessage({ messages: [msg], onFailed:()=>{} }); } } }); }, previewImage(e) { wx.previewImage({ urls: [e.currentTarget.dataset.url], current: e.currentTarget.dataset.url }); }, togglePlusPanel() { this.setData({ showPlusPanel: !this.data.showPlusPanel, showEmojiPanel: false }); }, closePlusPanel() { this.setData({ showPlusPanel: false }); }, openEmojiFromPlus() { this.setData({ showPlusPanel: false, showEmojiPanel: true }); }, toggleEmoji() { this.setData({ showEmojiPanel: !this.data.showEmojiPanel }); }, insertEmoji(e) { this.setData({ inputText: this.data.inputText + e.currentTarget.dataset.emoji }); }, closeEmojiPanel() { this.setData({ showEmojiPanel: false }); }, openOrderSender() { this.setData({ showOrderSender: true, showPlusPanel: false }); }, closeOrderSender() { this.setData({ showOrderSender: false }); }, onSendOrder(e) { const order = e.detail.order; if (!order) return; const text = `[่ฎขๅ•]${order.dingdan_id}\n${order.jieshao}\nยฅ${order.jine}`; this.setData({ inputText: text }); this.sendMessage(); }, onKeyboardHeightChange(e) { this.setData({ keyboardHeight: e.detail.height }); }, onPeerAvatarError() { this.setData({ toAvatar: getDefaultAvatarUrl(app) }); }, onSelfAvatarError(e) { const index = e.currentTarget.dataset.index; const def = getDefaultAvatarUrl(app); this.setData({ [`messages[${index}].senderData.avatar`]: def, 'currentUser.avatar': def, }); }, });