chatroom.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. const FATAL_REBUILD_TOLERANCE = 10
  2. const SETDATA_SCROLL_TO_BOTTOM = {
  3. scrollTop: 100000,
  4. scrollWithAnimation: true,
  5. }
  6. Component({
  7. properties: {
  8. envId: String,
  9. collection: String,
  10. groupId: String,
  11. groupName: String,
  12. userInfo: Object,
  13. onGetUserInfo: {
  14. type: Function,
  15. },
  16. getOpenID: {
  17. type: Function,
  18. },
  19. },
  20. data: {
  21. chats: [],
  22. textInputValue: '',
  23. openId: '',
  24. scrollTop: 0,
  25. scrollToMessage: '',
  26. hasKeyboard: false,
  27. },
  28. methods: {
  29. onGetUserInfo(e) {
  30. this.properties.onGetUserInfo(e)
  31. },
  32. getOpenID() {
  33. return this.properties.getOpenID()
  34. },
  35. mergeCommonCriteria(criteria) {
  36. return {
  37. groupId: this.data.groupId,
  38. ...criteria,
  39. }
  40. },
  41. async initRoom() {
  42. this.try(async () => {
  43. await this.initOpenID()
  44. const { envId, collection } = this.properties
  45. this.db = wx.cloud.database({
  46. env: envId,
  47. })
  48. const db = this.db
  49. const _ = db.command
  50. const { data: initList } = await db.collection(collection).where(this.mergeCommonCriteria()).orderBy('sendTimeTS', 'desc').get()
  51. console.log('init query chats', initList)
  52. this.setData({
  53. chats: initList.reverse(),
  54. scrollTop: 10000,
  55. })
  56. this.initWatch(initList.length ? {
  57. sendTimeTS: _.gt(initList[initList.length - 1].sendTimeTS),
  58. } : {})
  59. }, '初始化失败')
  60. },
  61. async initOpenID() {
  62. return this.try(async () => {
  63. const openId = await this.getOpenID()
  64. this.setData({
  65. openId,
  66. })
  67. }, '初始化 openId 失败')
  68. },
  69. async initWatch(criteria) {
  70. this.try(() => {
  71. const { collection } = this.properties
  72. const db = this.db
  73. const _ = db.command
  74. console.warn(`开始监听`, criteria)
  75. this.messageListener = db.collection(collection).where(this.mergeCommonCriteria(criteria)).watch({
  76. onChange: this.onRealtimeMessageSnapshot.bind(this),
  77. onError: e => {
  78. if (!this.inited || this.fatalRebuildCount >= FATAL_REBUILD_TOLERANCE) {
  79. this.showError(this.inited ? '监听错误,已断开' : '初始化监听失败', e, '重连', () => {
  80. this.initWatch(this.data.chats.length ? {
  81. sendTimeTS: _.gt(this.data.chats[this.data.chats.length - 1].sendTimeTS),
  82. } : {})
  83. })
  84. } else {
  85. this.initWatch(this.data.chats.length ? {
  86. sendTimeTS: _.gt(this.data.chats[this.data.chats.length - 1].sendTimeTS),
  87. } : {})
  88. }
  89. },
  90. })
  91. }, '初始化监听失败')
  92. },
  93. onRealtimeMessageSnapshot(snapshot) {
  94. console.warn(`收到消息`, snapshot)
  95. if (snapshot.type === 'init') {
  96. this.setData({
  97. chats: [
  98. ...this.data.chats,
  99. ...[...snapshot.docs].sort((x, y) => x.sendTimeTS - y.sendTimeTS),
  100. ],
  101. })
  102. this.scrollToBottom()
  103. this.inited = true
  104. } else {
  105. let hasNewMessage = false
  106. let hasOthersMessage = false
  107. const chats = [...this.data.chats]
  108. for (const docChange of snapshot.docChanges) {
  109. switch (docChange.queueType) {
  110. case 'enqueue': {
  111. hasOthersMessage = docChange.doc._openid !== this.data.openId
  112. const ind = chats.findIndex(chat => chat._id === docChange.doc._id)
  113. if (ind > -1) {
  114. if (chats[ind].msgType === 'image' && chats[ind].tempFilePath) {
  115. chats.splice(ind, 1, {
  116. ...docChange.doc,
  117. tempFilePath: chats[ind].tempFilePath,
  118. })
  119. } else chats.splice(ind, 1, docChange.doc)
  120. } else {
  121. hasNewMessage = true
  122. chats.push(docChange.doc)
  123. }
  124. break
  125. }
  126. }
  127. }
  128. this.setData({
  129. chats: chats.sort((x, y) => x.sendTimeTS - y.sendTimeTS),
  130. })
  131. if (hasOthersMessage || hasNewMessage) {
  132. this.scrollToBottom()
  133. }
  134. }
  135. },
  136. async onConfirmSendText(e) {
  137. this.try(async () => {
  138. if (!e.detail.value) {
  139. return
  140. }
  141. const { collection } = this.properties
  142. const db = this.db
  143. const _ = db.command
  144. const doc = {
  145. _id: `${Math.random()}_${Date.now()}`,
  146. groupId: this.data.groupId,
  147. avatar: this.data.userInfo.avatarUrl,
  148. nickName: this.data.userInfo.nickName,
  149. msgType: 'text',
  150. textContent: e.detail.value,
  151. sendTime: new Date(),
  152. sendTimeTS: Date.now(), // fallback
  153. }
  154. this.setData({
  155. textInputValue: '',
  156. chats: [
  157. ...this.data.chats,
  158. {
  159. ...doc,
  160. _openid: this.data.openId,
  161. writeStatus: 'pending',
  162. },
  163. ],
  164. })
  165. this.scrollToBottom(true)
  166. await db.collection(collection).add({
  167. data: doc,
  168. })
  169. this.setData({
  170. chats: this.data.chats.map(chat => {
  171. if (chat._id === doc._id) {
  172. return {
  173. ...chat,
  174. writeStatus: 'written',
  175. }
  176. } else return chat
  177. }),
  178. })
  179. }, '发送文字失败')
  180. },
  181. async onChooseImage(e) {
  182. wx.chooseImage({
  183. count: 1,
  184. sourceType: ['album', 'camera'],
  185. success: async res => {
  186. const { envId, collection } = this.properties
  187. const doc = {
  188. _id: `${Math.random()}_${Date.now()}`,
  189. groupId: this.data.groupId,
  190. avatar: this.data.userInfo.avatarUrl,
  191. nickName: this.data.userInfo.nickName,
  192. msgType: 'image',
  193. sendTime: new Date(),
  194. sendTimeTS: Date.now(), // fallback
  195. }
  196. this.setData({
  197. chats: [
  198. ...this.data.chats,
  199. {
  200. ...doc,
  201. _openid: this.data.openId,
  202. tempFilePath: res.tempFilePaths[0],
  203. writeStatus: 0,
  204. },
  205. ]
  206. })
  207. this.scrollToBottom(true)
  208. const uploadTask = wx.cloud.uploadFile({
  209. cloudPath: `${this.data.openId}/${Math.random()}_${Date.now()}.${res.tempFilePaths[0].match(/\.(\w+)$/)[1]}`,
  210. filePath: res.tempFilePaths[0],
  211. config: {
  212. env: envId,
  213. },
  214. success: res => {
  215. this.try(async () => {
  216. await this.db.collection(collection).add({
  217. data: {
  218. ...doc,
  219. imgFileID: res.fileID,
  220. },
  221. })
  222. }, '发送图片失败')
  223. },
  224. fail: e => {
  225. this.showError('发送图片失败', e)
  226. },
  227. })
  228. uploadTask.onProgressUpdate(({ progress }) => {
  229. this.setData({
  230. chats: this.data.chats.map(chat => {
  231. if (chat._id === doc._id) {
  232. return {
  233. ...chat,
  234. writeStatus: progress,
  235. }
  236. } else return chat
  237. })
  238. })
  239. })
  240. },
  241. })
  242. },
  243. onMessageImageTap(e) {
  244. wx.previewImage({
  245. urls: [e.target.dataset.fileid],
  246. })
  247. },
  248. scrollToBottom(force) {
  249. if (force) {
  250. console.log('force scroll to bottom')
  251. this.setData(SETDATA_SCROLL_TO_BOTTOM)
  252. return
  253. }
  254. this.createSelectorQuery().select('.body').boundingClientRect(bodyRect => {
  255. this.createSelectorQuery().select(`.body`).scrollOffset(scroll => {
  256. if (scroll.scrollTop + bodyRect.height * 3 > scroll.scrollHeight) {
  257. console.log('should scroll to bottom')
  258. this.setData(SETDATA_SCROLL_TO_BOTTOM)
  259. }
  260. }).exec()
  261. }).exec()
  262. },
  263. async onScrollToUpper() {
  264. if (this.db && this.data.chats.length) {
  265. const { collection } = this.properties
  266. const _ = this.db.command
  267. const { data } = await this.db.collection(collection).where(this.mergeCommonCriteria({
  268. sendTimeTS: _.lt(this.data.chats[0].sendTimeTS),
  269. })).orderBy('sendTimeTS', 'desc').get()
  270. this.data.chats.unshift(...data.reverse())
  271. this.setData({
  272. chats: this.data.chats,
  273. scrollToMessage: `item-${data.length}`,
  274. scrollWithAnimation: false,
  275. })
  276. }
  277. },
  278. async try(fn, title) {
  279. try {
  280. await fn()
  281. } catch (e) {
  282. this.showError(title, e)
  283. }
  284. },
  285. showError(title, content, confirmText, confirmCallback) {
  286. console.error(title, content)
  287. wx.showModal({
  288. title,
  289. content: content.toString(),
  290. showCancel: confirmText ? true : false,
  291. confirmText,
  292. success: res => {
  293. res.confirm && confirmCallback()
  294. },
  295. })
  296. },
  297. },
  298. ready() {
  299. global.chatroom = this
  300. this.initRoom()
  301. this.fatalRebuildCount = 0
  302. },
  303. })