const FATAL_REBUILD_TOLERANCE = 10 const SETDATA_SCROLL_TO_BOTTOM = { scrollTop: 100000, scrollWithAnimation: true, } Component({ properties: { envId: String, collection: String, groupId: String, groupName: String, userInfo: Object, onGetUserInfo: { type: Function, }, getOpenID: { type: Function, }, }, data: { chats: [], textInputValue: '', openId: '', scrollTop: 0, scrollToMessage: '', hasKeyboard: false, }, methods: { onGetUserInfo(e) { this.properties.onGetUserInfo(e) }, getOpenID() { return this.properties.getOpenID() }, mergeCommonCriteria(criteria) { return { groupId: this.data.groupId, ...criteria, } }, async initRoom() { this.try(async () => { await this.initOpenID() const { envId, collection } = this.properties this.db = wx.cloud.database({ env: envId, }) const db = this.db const _ = db.command const { data: initList } = await db.collection(collection).where(this.mergeCommonCriteria()).orderBy('sendTimeTS', 'desc').get() console.log('init query chats', initList) this.setData({ chats: initList.reverse(), scrollTop: 10000, }) this.initWatch(initList.length ? { sendTimeTS: _.gt(initList[initList.length - 1].sendTimeTS), } : {}) }, '初始化失败') }, async initOpenID() { return this.try(async () => { const openId = await this.getOpenID() this.setData({ openId, }) }, '初始化 openId 失败') }, async initWatch(criteria) { this.try(() => { const { collection } = this.properties const db = this.db const _ = db.command console.warn(`开始监听`, criteria) this.messageListener = db.collection(collection).where(this.mergeCommonCriteria(criteria)).watch({ onChange: this.onRealtimeMessageSnapshot.bind(this), onError: e => { if (!this.inited || this.fatalRebuildCount >= FATAL_REBUILD_TOLERANCE) { this.showError(this.inited ? '监听错误,已断开' : '初始化监听失败', e, '重连', () => { this.initWatch(this.data.chats.length ? { sendTimeTS: _.gt(this.data.chats[this.data.chats.length - 1].sendTimeTS), } : {}) }) } else { this.initWatch(this.data.chats.length ? { sendTimeTS: _.gt(this.data.chats[this.data.chats.length - 1].sendTimeTS), } : {}) } }, }) }, '初始化监听失败') }, onRealtimeMessageSnapshot(snapshot) { console.warn(`收到消息`, snapshot) if (snapshot.type === 'init') { this.setData({ chats: [ ...this.data.chats, ...[...snapshot.docs].sort((x, y) => x.sendTimeTS - y.sendTimeTS), ], }) this.scrollToBottom() this.inited = true } else { let hasNewMessage = false let hasOthersMessage = false const chats = [...this.data.chats] for (const docChange of snapshot.docChanges) { switch (docChange.queueType) { case 'enqueue': { hasOthersMessage = docChange.doc._openid !== this.data.openId const ind = chats.findIndex(chat => chat._id === docChange.doc._id) if (ind > -1) { if (chats[ind].msgType === 'image' && chats[ind].tempFilePath) { chats.splice(ind, 1, { ...docChange.doc, tempFilePath: chats[ind].tempFilePath, }) } else chats.splice(ind, 1, docChange.doc) } else { hasNewMessage = true chats.push(docChange.doc) } break } } } this.setData({ chats: chats.sort((x, y) => x.sendTimeTS - y.sendTimeTS), }) if (hasOthersMessage || hasNewMessage) { this.scrollToBottom() } } }, async onConfirmSendText(e) { this.try(async () => { if (!e.detail.value) { return } const { collection } = this.properties const db = this.db const _ = db.command const doc = { _id: `${Math.random()}_${Date.now()}`, groupId: this.data.groupId, avatar: this.data.userInfo.avatarUrl, nickName: this.data.userInfo.nickName, msgType: 'text', textContent: e.detail.value, sendTime: new Date(), sendTimeTS: Date.now(), // fallback } this.setData({ textInputValue: '', chats: [ ...this.data.chats, { ...doc, _openid: this.data.openId, writeStatus: 'pending', }, ], }) this.scrollToBottom(true) await db.collection(collection).add({ data: doc, }) this.setData({ chats: this.data.chats.map(chat => { if (chat._id === doc._id) { return { ...chat, writeStatus: 'written', } } else return chat }), }) }, '发送文字失败') }, async onChooseImage(e) { wx.chooseImage({ count: 1, sourceType: ['album', 'camera'], success: async res => { const { envId, collection } = this.properties const doc = { _id: `${Math.random()}_${Date.now()}`, groupId: this.data.groupId, avatar: this.data.userInfo.avatarUrl, nickName: this.data.userInfo.nickName, msgType: 'image', sendTime: new Date(), sendTimeTS: Date.now(), // fallback } this.setData({ chats: [ ...this.data.chats, { ...doc, _openid: this.data.openId, tempFilePath: res.tempFilePaths[0], writeStatus: 0, }, ] }) this.scrollToBottom(true) const uploadTask = wx.cloud.uploadFile({ cloudPath: `${this.data.openId}/${Math.random()}_${Date.now()}.${res.tempFilePaths[0].match(/\.(\w+)$/)[1]}`, filePath: res.tempFilePaths[0], config: { env: envId, }, success: res => { this.try(async () => { await this.db.collection(collection).add({ data: { ...doc, imgFileID: res.fileID, }, }) }, '发送图片失败') }, fail: e => { this.showError('发送图片失败', e) }, }) uploadTask.onProgressUpdate(({ progress }) => { this.setData({ chats: this.data.chats.map(chat => { if (chat._id === doc._id) { return { ...chat, writeStatus: progress, } } else return chat }) }) }) }, }) }, onMessageImageTap(e) { wx.previewImage({ urls: [e.target.dataset.fileid], }) }, scrollToBottom(force) { if (force) { console.log('force scroll to bottom') this.setData(SETDATA_SCROLL_TO_BOTTOM) return } this.createSelectorQuery().select('.body').boundingClientRect(bodyRect => { this.createSelectorQuery().select(`.body`).scrollOffset(scroll => { if (scroll.scrollTop + bodyRect.height * 3 > scroll.scrollHeight) { console.log('should scroll to bottom') this.setData(SETDATA_SCROLL_TO_BOTTOM) } }).exec() }).exec() }, async onScrollToUpper() { if (this.db && this.data.chats.length) { const { collection } = this.properties const _ = this.db.command const { data } = await this.db.collection(collection).where(this.mergeCommonCriteria({ sendTimeTS: _.lt(this.data.chats[0].sendTimeTS), })).orderBy('sendTimeTS', 'desc').get() this.data.chats.unshift(...data.reverse()) this.setData({ chats: this.data.chats, scrollToMessage: `item-${data.length}`, scrollWithAnimation: false, }) } }, async try(fn, title) { try { await fn() } catch (e) { this.showError(title, e) } }, showError(title, content, confirmText, confirmCallback) { console.error(title, content) wx.showModal({ title, content: content.toString(), showCancel: confirmText ? true : false, confirmText, success: res => { res.confirm && confirmCallback() }, }) }, }, ready() { global.chatroom = this this.initRoom() this.fatalRebuildCount = 0 }, })