direct_conversation_view.js (13488B)
1 import { throttle, xor } from 'lodash' 2 import DirectConversationStatus from '../direct_conversation_status/direct_conversation_status.vue' 3 import DirectConversationAvatar from '../direct_conversation_avatar/direct_conversation_avatar.vue' 4 import PostStatusForm from '../post_status_form/post_status_form.vue' 5 import DirectConversationTitle from '../direct_conversation_title/direct_conversation_title.vue' 6 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' 7 import DirectConversationInfoPage from '../direct_conversation_info_page/direct_conversation_info_page.vue' 8 import directConversationService from '../../services/direct_conversation_service/direct_conversation_service.js' 9 10 const conversation = { 11 props: [ 12 'isInfo', 13 'users' 14 ], 15 components: { 16 DirectConversationStatus, 17 DirectConversationTitle, 18 DirectConversationAvatar, 19 PostStatusForm, 20 DirectConversationInfoPage 21 }, 22 data () { 23 return { 24 loadingStatuses: true, 25 loadingConversation: false, 26 editedStatusId: undefined, 27 chatParticipants: [], 28 fetcher: undefined, 29 jumpToBottomButtonVisible: false, 30 mobileLayout: this.$store.state.interface.mobileLayout, 31 conversationId: this.$route.params.id !== 'new' && this.$route.params.id, 32 hoveredSequenceId: undefined 33 } 34 }, 35 created () { 36 this.startFetching() 37 window.addEventListener('resize', this.handleLayoutChange) 38 }, 39 mounted () { 40 this.$nextTick(() => { 41 let scrollable = this.$refs.scrollable 42 if (scrollable) { 43 window.addEventListener('scroll', this.handleScroll) 44 } 45 this.updateSize() 46 }) 47 if (this.isMobileLayout) { 48 this.setMobileChatLayout() 49 } 50 51 if (typeof document.hidden !== 'undefined') { 52 document.addEventListener('visibilitychange', this.handleVisibilityChange, false) 53 this.$store.commit('setDirectConversationFocused', !document.hidden) 54 } 55 }, 56 destroyed () { 57 window.removeEventListener('scroll', this.handleScroll) 58 window.removeEventListener('resize', this.handleLayoutChange) 59 this.unsetMobileChatLayout() 60 if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false) 61 this.$store.dispatch('clearCurrentDirectConversation') 62 }, 63 computed: { 64 formattedStatuses () { 65 return directConversationService.getView(this.$store.state.directConversations.currentDirectConversation) 66 }, 67 isMobileLayout () { 68 return this.$store.state.interface.mobileLayout 69 }, 70 currentUser () { 71 return this.$store.state.users.currentUser 72 }, 73 newMessagesCount () { 74 return this.$store.state.directConversations.currentDirectConversation.newMessagesCount 75 }, 76 isNewChat () { 77 return (this.$route.params.id === 'new' || !this.$route.params.id) && this.queryUsers.length > 0 78 }, 79 queryUsers () { 80 let users = this.$route.query.users 81 if (Array.isArray(users)) { 82 return users 83 } else if (users) { 84 return [users] 85 } 86 return [] 87 }, 88 formPlaceholder () { 89 const names = this.participantNames 90 return this.$tc('direct_conversations.form_placeholder', names.length, [names.join(', ')]) 91 }, 92 participantNames () { 93 return this.chatParticipants.filter(recipient => recipient.id !== this.currentUser.id).map(r => r.screen_name) 94 }, 95 formScopeNotice () { 96 const names = this.participantNames 97 return names.length > 1 && this.$t('direct_conversations.scope_notice', [names.join(', ')]) 98 } 99 }, 100 watch: { 101 formattedStatuses () { 102 let bottomedOut = this.bottomedOut(50) 103 this.$nextTick(() => { 104 if (bottomedOut) { 105 this.scrollDown({ forceRead: true }) 106 } 107 }) 108 }, 109 isInfo () { 110 this.$nextTick(() => { 111 this.updateSize() 112 if (!this.isInfo) { 113 this.scrollDown() 114 } 115 }) 116 } 117 }, 118 methods: { 119 onStatusHover ({ state, sequenceId }) { 120 this.hoveredSequenceId = state ? sequenceId : undefined 121 }, 122 onPosted (data) { 123 if (!this.conversationId) { 124 this.conversationId = data.direct_conversation_id 125 this.doStartFetching() 126 } 127 this.$store.dispatch('addToCurrentDirectConversationStatuses', { statuses: [data] }).then(() => { 128 this.updateSize() 129 this.scrollDown({ forceRead: true }) 130 }) 131 }, 132 onScopeNoticeDismissed () { 133 this.$nextTick(() => { 134 this.updateSize() 135 }) 136 }, 137 onFilesDropped () { 138 this.$nextTick(() => { 139 this.updateSize() 140 }) 141 }, 142 handleVisibilityChange () { 143 this.$store.commit('setDirectConversationFocused', !document.hidden) 144 }, 145 onToggleActions (editedStatusId) { 146 if (this.editedStatusId === editedStatusId) { 147 this.editedStatusId = undefined 148 } else { 149 this.editedStatusId = editedStatusId 150 } 151 }, 152 handleLayoutChange () { 153 this.updateSize() 154 let mobileLayout = this.isMobileLayout 155 if (this.mobileLayout !== mobileLayout) { 156 if (this.mobileLayout === false && mobileLayout === true) { 157 this.setMobileChatLayout() 158 } 159 if (this.mobileLayout === true && mobileLayout === false) { 160 this.unsetMobileChatLayout() 161 } 162 this.mobileLayout = this.isMobileLayout 163 this.$nextTick(() => { 164 this.updateSize() 165 this.scrollDown() 166 }) 167 } 168 }, 169 setMobileChatLayout () { 170 // This is a hacky way to adjust the global layout to the mobile chat (without modifying the rest of the app). 171 // This layout prevents empty spaces from being visible at the bottom 172 // of the chat on iOS Safari (`safe-area-inset`) when 173 // - the on-screen keyboard appears and the user starts typing 174 // - the user selects the text inside the input area 175 // - the user selects and deletes the text that is multiple lines long 176 // TODO: unify the chat layout with the global layout. 177 178 let html = document.querySelector('html') 179 if (html) { 180 html.style.overflow = 'hidden' 181 html.style.height = '100%' 182 } 183 184 let body = document.querySelector('body') 185 if (body) { 186 body.style.height = '100%' 187 body.style.overscrollBehavior = 'none' 188 } 189 190 let app = document.getElementById('app') 191 if (app) { 192 app.style.height = '100%' 193 app.style.overflow = 'hidden' 194 app.style.minHeight = 'auto' 195 } 196 197 let appBgWrapper = window.document.getElementById('app_bg_wrapper') 198 if (appBgWrapper) { 199 appBgWrapper.style.overflow = 'hidden' 200 } 201 202 let main = document.getElementsByClassName('main')[0] 203 if (main) { 204 main.style.overflow = 'hidden' 205 main.style.height = '100%' 206 } 207 208 let content = document.getElementById('content') 209 if (content) { 210 content.style.paddingTop = '0' 211 content.style.height = '100%' 212 content.style.overflow = 'visible' 213 } 214 215 this.$nextTick(() => { 216 this.updateSize() 217 }) 218 }, 219 unsetMobileChatLayout () { 220 let html = document.querySelector('html') 221 if (html) { 222 html.style.overflow = 'visible' 223 html.style.height = 'unset' 224 } 225 226 let body = document.querySelector('body') 227 if (body) { 228 body.style.height = 'unset' 229 body.style.overscrollBehavior = 'unset' 230 } 231 232 let app = document.getElementById('app') 233 if (app) { 234 app.style.height = '100%' 235 app.style.overflow = 'visible' 236 app.style.minHeight = '100vh' 237 } 238 239 let appBgWrapper = document.getElementById('app_bg_wrapper') 240 if (appBgWrapper) { 241 appBgWrapper.style.overflow = 'visible' 242 } 243 244 let main = document.getElementsByClassName('main')[0] 245 if (main) { 246 main.style.overflow = 'visible' 247 main.style.height = 'unset' 248 } 249 250 let content = document.getElementById('content') 251 if (content) { 252 content.style.paddingTop = '60px' 253 content.style.height = 'unset' 254 content.style.overflow = 'unset' 255 } 256 }, 257 handleResize (newHeight) { 258 this.updateSize(newHeight) 259 }, 260 updateSize (newHeight, _diff) { 261 let h = this.$refs.header 262 let s = this.$refs.scrollable 263 let f = this.$refs.footer 264 if (h && s && f) { 265 let height = 0 266 if (this.isMobileLayout) { 267 height = parseFloat(getComputedStyle(window.document.body, null).height.replace('px', '')) 268 let newHeight = (height - h.clientHeight - f.clientHeight) 269 s.style.height = newHeight + 'px' 270 } else { 271 height = parseFloat(getComputedStyle(this.$refs.inner, null).height.replace('px', '')) 272 let newHeight = (height - h.clientHeight - f.clientHeight) 273 s.style.height = newHeight + 'px' 274 } 275 } 276 }, 277 scrollDown (options = {}) { 278 let { behavior = 'auto', forceRead = false } = options 279 let container = this.$refs.scrollable 280 let scrollable = this.$refs.scrollable 281 this.doScrollDown(scrollable, container, behavior) 282 if (forceRead || this.newMessagesCount > 0) { 283 this.readConversation() 284 } 285 }, 286 doScrollDown (scrollable, container, behavior) { 287 if (!container) { return } 288 setTimeout(() => { 289 scrollable.scrollTo({ top: container.scrollHeight, left: 0, behavior }) 290 }, 100) 291 }, 292 bottomedOut (offset) { 293 let bottomedOut = false 294 295 if (this.$refs.scrollable) { 296 let scrollHeight = this.$refs.scrollable.scrollTop + (offset || 0) 297 let totalHeight = this.$refs.scrollable.scrollHeight - this.$refs.scrollable.offsetHeight 298 bottomedOut = totalHeight <= scrollHeight 299 } 300 301 return bottomedOut 302 }, 303 handleScroll: throttle(function () { 304 if (this.bottomedOut(150)) { 305 this.jumpToBottomButtonVisible = false 306 let newMessagesCount = this.$store.state.directConversations.currentDirectConversation.newMessagesCount 307 if (newMessagesCount > 0) { 308 this.readConversation() 309 } else { 310 this.$store.dispatch('resetDirectConversationNewMessageCount') 311 } 312 } else { 313 this.jumpToBottomButtonVisible = true 314 } 315 }, 100), 316 goBack () { 317 this.$router.go(-1) 318 }, 319 fetchConversation (isFirstFetch, conversationId) { 320 this.$store.state.api.backendInteractor.directConversationTimeline({ id: conversationId }) 321 .then((statuses) => { 322 let bottomedOut = this.bottomedOut() 323 this.$store.dispatch('addToCurrentDirectConversationStatuses', { statuses, check: true, isFirstFetch }).then(() => { 324 if (isFirstFetch) { 325 setTimeout(() => { 326 this.updateSize() 327 this.scrollDown({ forceRead: true }) 328 }, 200) 329 } else if (bottomedOut) { 330 this.scrollDown() 331 } 332 setTimeout(() => { 333 this.loadingStatuses = false 334 }, 1000) 335 }) 336 }) 337 }, 338 fetchConversationInfo () { 339 this.$store.state.api.backendInteractor.directConversation({ id: this.conversationId }) 340 .then(resp => { 341 if (resp.accounts.length > 0) { 342 this.chatParticipants = resp.accounts 343 } else { 344 this.chatParticipants = [this.currentUser] 345 } 346 }) 347 }, 348 readConversation () { 349 if (!this.conversationId) { return } 350 this.$store.dispatch('readDirectConversation', { id: this.conversationId }) 351 }, 352 async resolveUsers () { 353 let userIds = this.queryUsers 354 let users = userIds.map(id => { 355 let user = this.$store.getters.findUser(id) 356 if (user) { 357 return Promise.resolve(user) 358 } else { 359 return this.$store.state.api.backendInteractor.fetchUser({ id }) 360 } 361 }) 362 363 let result = await Promise.all(users) 364 return result.filter(u => u) 365 }, 366 startFetching () { 367 if (this.isNewChat) { 368 this.resolveUsers().then(users => { 369 this.chatParticipants = users 370 let recipients = users.map(u => u.id) 371 this.loadingConversation = true 372 this.$store.state.api.backendInteractor.directConversations({ recipients, limit: 1 }).then(resp => { 373 this.loadingConversation = false 374 let conversation = resp.directConversations[0] 375 let validConversation = conversation && xor(conversation.accounts.map(a => a.id), recipients).length === 0 376 if (validConversation) { 377 this.conversationId = conversation.id 378 this.doStartFetching() 379 } 380 }) 381 }) 382 } else { 383 this.doStartFetching() 384 } 385 }, 386 doStartFetching () { 387 let conversationId = parseInt(this.conversationId, 10) 388 this.$store.dispatch('startFetchingCurrentDirectConversation', { 389 conversationId, 390 fetcher: () => setInterval(() => this.fetchConversation(false, conversationId), 5000) 391 }) 392 this.fetchConversationInfo() 393 this.fetchConversation(true, conversationId) 394 }, 395 userProfileLink (user) { 396 return this.generateUserProfileLink(user.id, user.screen_name) 397 }, 398 generateUserProfileLink (id, name) { 399 return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) 400 } 401 } 402 } 403 404 export default conversation