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