1
1
<template >
2
- <div class =" a-chat " >
3
- <div class =" a-chat__content " >
2
+ <div : class =" classes.root " >
3
+ <div : class =" classes.content " >
4
4
<slot name =" header" />
5
5
6
6
<v-divider />
7
7
8
- <div class =" a-chat__body " >
8
+ <div : class =" classes.body " >
9
9
<div class =" text-center py-2" >
10
10
<v-progress-circular
11
11
v-show =" loading"
16
16
/>
17
17
</div >
18
18
19
- <div ref =" messages " class =" a-chat__body-messages " >
19
+ <div ref =" messagesRef " : class =" classes.bodyMessages " >
20
20
<template v-for =" message in messages " :key =" message .id " >
21
21
<slot
22
22
name =" message"
28
28
</template >
29
29
</div >
30
30
31
- <div class =" a-chat__fab " >
31
+ <div : class =" classes.fab " >
32
32
<slot name =" fab" />
33
33
</div >
34
34
</div >
35
35
36
36
<slot name =" form" />
37
37
</div >
38
38
39
- <div v-if =" $slots.overlay" class =" a-chat__overlay " >
39
+ <div v-if =" $slots.overlay" : class =" classes.overlay " >
40
40
<slot name =" overlay" />
41
41
</div >
42
42
</div >
43
43
</template >
44
44
45
- <script >
45
+ <script lang="ts">
46
+ import { ref , onMounted , onBeforeUnmount , computed , defineComponent , PropType } from ' vue'
46
47
import throttle from ' lodash/throttle'
47
48
import scrollIntoView from ' scroll-into-view-if-needed'
48
49
import Styler from ' stylefire'
49
50
import { animate } from ' popmotion'
50
-
51
51
import { SCROLL_TO_REPLIED_MESSAGE_ANIMATION_DURATION } from ' @/lib/constants'
52
52
import { isStringEqualCI } from ' @/lib/textHelpers'
53
+ import { isWelcomeChat as checkIsWelcomeChat } from ' @/lib/chat/meta/utils/isWelcomeChat'
54
+ import { NormalizedChatMessageTransaction } from ' @/lib/chat/helpers'
55
+ import { User } from ' @/components/AChat/types'
56
+
57
+ const className = ' a-chat'
58
+ const classes = {
59
+ root: className ,
60
+ content: ` ${className }__content ` ,
61
+ body: ` ${className }__body ` ,
62
+ bodyMessages: ` ${className }__body-messages ` ,
63
+ fab: ` ${className }__fab ` ,
64
+ overlay: ` ${className }__overlay `
65
+ }
53
66
54
- const emitScroll = throttle (function () {
55
- this .$emit (' scroll' , this .currentScrollTop , this .isScrolledToBottom ())
56
- }, 200 )
57
-
58
- export default {
67
+ export default defineComponent ({
59
68
props: {
60
69
messages: {
61
- type: Array ,
70
+ type: Array as PropType < NormalizedChatMessageTransaction []> ,
62
71
default : () => []
63
72
},
64
73
partners: {
65
- type: Array ,
74
+ type: Array as PropType < User []> ,
66
75
default : () => []
67
76
},
68
77
userId: {
69
- type: String
78
+ type: String ,
79
+ required: true
70
80
},
71
81
loading: {
72
82
type: Boolean ,
@@ -78,128 +88,129 @@ export default {
78
88
}
79
89
},
80
90
emits: [' scroll' , ' scroll:bottom' , ' scroll:top' ],
81
- data : () => ({
82
- currentScrollHeight: 0 ,
83
- currentScrollTop: 0 ,
84
- currentClientHeight: 0
85
- }),
86
- mounted () {
87
- this .attachScrollListener ()
88
-
89
- this .currentClientHeight = this .$refs .messages .clientHeight
91
+ setup(props , { emit }) {
92
+ const messagesRef = ref <HTMLDivElement | null >(null )
93
+ const currentScrollHeight = ref (0 )
94
+ const currentScrollTop = ref (0 )
95
+ const currentClientHeight = ref (0 )
96
+
90
97
const resizeHandler = () => {
91
- const clientHeightDelta = this .currentClientHeight - this .$refs .messages .clientHeight
98
+ if (! messagesRef .value ) return
99
+
100
+ const clientHeightDelta = currentClientHeight .value - messagesRef .value .clientHeight
92
101
93
102
const nonVisibleClientHeight =
94
- this . $refs . messages .scrollHeight -
95
- this . $refs . messages .clientHeight -
96
- Math .ceil (this . $refs . messages .scrollTop )
103
+ messagesRef . value .scrollHeight -
104
+ messagesRef . value .clientHeight -
105
+ Math .ceil (messagesRef . value .scrollTop )
97
106
const scrolledToBottom = nonVisibleClientHeight === 0
98
107
99
- if (scrolledToBottom) {
100
- // Browser updates Element.scrollTop by itself
101
- } else {
102
- this .$refs .messages .scrollTop += clientHeightDelta
108
+ if (! scrolledToBottom ) {
109
+ messagesRef .value .scrollTop += clientHeightDelta
103
110
}
104
111
105
- this . currentClientHeight = this . $refs . messages .clientHeight
112
+ currentClientHeight . value = messagesRef . value .clientHeight
106
113
}
107
114
108
- this .resizeObserver = new ResizeObserver (resizeHandler)
109
- this .resizeObserver .observe (this .$refs .messages )
110
- },
111
- beforeUnmount () {
112
- this .destroyScrollListener ()
113
- this .resizeObserver ? .unobserve (this .$refs .messages )
114
- },
115
- methods: {
116
- attachScrollListener () {
117
- this .$refs .messages .addEventListener (' scroll' , this .onScroll )
118
- },
115
+ const resizeObserver = ref (new ResizeObserver (resizeHandler ))
119
116
120
- destroyScrollListener () {
121
- this .$refs .messages .removeEventListener (' scroll' , this .onScroll )
122
- },
117
+ const isWelcomeChat = computed (() => {
118
+ return props .partners
119
+ .map ((item ) => item .id )
120
+ .map (checkIsWelcomeChat )
121
+ .reduce ((previous , current ) => previous || current , false )
122
+ })
123
123
124
- onScroll () {
125
- const scrollHeight = this .$refs .messages .scrollHeight
126
- const scrollTop = Math .ceil (this .$refs .messages .scrollTop )
127
- const clientHeight = this .$refs .messages .clientHeight
124
+ const emitScroll = throttle (
125
+ () => emit (' scroll' , currentScrollTop .value , isScrolledToBottom ()),
126
+ 200
127
+ )
128
+
129
+ const attachScrollListener = () => {
130
+ messagesRef .value ?.addEventListener (' scroll' , onScroll )
131
+ }
132
+
133
+ const destroyScrollListener = () => {
134
+ messagesRef .value ?.removeEventListener (' scroll' , onScroll )
135
+ }
136
+
137
+ const onScroll = () => {
138
+ if (! messagesRef .value ) return
139
+
140
+ const scrollHeight = messagesRef .value .scrollHeight
141
+ const scrollTop = Math .ceil (messagesRef .value .scrollTop )
142
+ const clientHeight = messagesRef .value .clientHeight
128
143
129
- // Scrolled to Bottom
130
144
if (scrollHeight - scrollTop === clientHeight ) {
131
- this . $ emit (' scroll:bottom' )
145
+ emit (' scroll:bottom' )
132
146
} else if (scrollTop === 0 ) {
133
- // Scrolled to Top
134
- // Save current `scrollHeight` to maintain scroll
135
- // position when unshift new messages
136
- this .currentScrollHeight = scrollHeight
137
- this .$emit (' scroll:top' )
147
+ currentScrollHeight .value = scrollHeight
148
+ emit (' scroll:top' )
138
149
}
139
150
140
- // Save previous values of `scrollTop` and `scrollHeight`
141
- // Needed for keeping the same scroll position when prepending
142
- // new messages to the chat
143
- this .currentScrollTop = scrollTop
144
- this .currentScrollHeight = scrollHeight
151
+ currentScrollTop .value = scrollTop
152
+ currentScrollHeight .value = scrollHeight
145
153
146
- emitScroll . call ( this )
147
- },
154
+ emitScroll ( )
155
+ }
148
156
149
- // Fix scroll position after unshift new messages.
150
- // Called from parent component.
151
- maintainScrollPosition () {
152
- this .$refs .messages .scrollTop =
153
- this .$refs .messages .scrollHeight - this .currentScrollHeight + this .currentScrollTop
154
- },
157
+ const maintainScrollPosition = () => {
158
+ if (! messagesRef .value ) return
155
159
156
- // Scroll to Bottom when new message.
157
- // Called from parent component.
158
- scrollToBottom () {
159
- this .$refs .messages .scrollTop = this .$refs .messages .scrollHeight
160
- },
160
+ if (isWelcomeChat .value ) {
161
+ messagesRef .value .scrollTop = 0
162
+ return
163
+ }
161
164
162
- scrollTo (position ) {
163
- this .$refs .messages .scrollTop = position
164
- },
165
+ messagesRef .value .scrollTop =
166
+ messagesRef .value .scrollHeight - currentScrollHeight .value + currentScrollTop .value
167
+ }
168
+
169
+ const scrollToBottom = () => {
170
+ if (messagesRef .value ) {
171
+ messagesRef .value .scrollTop = messagesRef .value .scrollHeight
172
+ }
173
+ }
174
+
175
+ const scrollTo = (position : number ) => {
176
+ if (messagesRef .value ) {
177
+ messagesRef .value .scrollTop = position
178
+ }
179
+ }
180
+
181
+ const scrollToMessage = (index : number ) => {
182
+ if (! messagesRef .value ) return
165
183
166
- /**
167
- * Scroll to message by index, starting with the last.
168
- */
169
- scrollToMessage (index ) {
170
- const elements = this .$refs .messages .children
184
+ const elements = messagesRef .value .children
171
185
172
186
if (! elements ) return
173
187
174
- const element = elements[elements .length - 1 - index]
188
+ const element = elements [elements .length - 1 - index ] as HTMLElement
175
189
176
190
if (element ) {
177
- this . $refs . messages .scrollTop = element .offsetTop - 16
191
+ messagesRef . value .scrollTop = element .offsetTop - 16
178
192
} else {
179
- this . scrollToBottom ()
193
+ scrollToBottom ()
180
194
}
181
- },
195
+ }
196
+
197
+ const scrollToMessageEasy = async (index : number ): Promise <boolean > => {
198
+ if (! messagesRef .value ) return false
182
199
183
- /**
184
- * Smooth scroll to message by index (starting with the last).
185
- * @returns Promise<boolean> If `true` then scrolling has been applied.
186
- */
187
- scrollToMessageEasy (index ) {
188
- const elements = this .$refs .messages .children
200
+ const elements = messagesRef .value .children
189
201
190
- if (! elements) return Promise . resolve ( false )
202
+ if (! elements ) return false
191
203
192
- const element = elements[elements .length - 1 - index]
204
+ const element = elements [elements .length - 1 - index ] as HTMLElement
193
205
194
- if (! element) return Promise . resolve ( false )
206
+ if (! element ) return false
195
207
196
208
return new Promise ((resolve ) => {
197
209
scrollIntoView (element , {
198
210
behavior : (instructions ) => {
199
211
const [{ el, top }] = instructions
200
212
const styler = Styler (el )
201
213
202
- // do nothing if the element is already scrolled at target position
203
214
if (el .scrollTop === top ) {
204
215
resolve (false )
205
216
return
@@ -216,25 +227,50 @@ export default {
216
227
block: ' center'
217
228
})
218
229
})
219
- },
230
+ }
231
+
232
+ const isScrolledToBottom = () => {
233
+ if (! messagesRef .value ) return false
220
234
221
- isScrolledToBottom () {
222
235
const scrollOffset =
223
- this . $refs . messages .scrollHeight -
224
- Math .ceil (this . $refs . messages .scrollTop ) -
225
- this . $refs . messages .clientHeight
236
+ messagesRef . value .scrollHeight -
237
+ Math .ceil (messagesRef . value .scrollTop ) -
238
+ messagesRef . value .clientHeight
226
239
227
240
return scrollOffset <= 60
228
- },
241
+ }
229
242
230
- /**
231
- * Returns sender address and name.
232
- * @param {string} senderId Sender address
233
- * @returns {{ id: string, name: string }}
234
- */
235
- getSenderMeta (senderId ) {
236
- return this .partners .find ((partner ) => isStringEqualCI (partner .id , senderId))
243
+ const getSenderMeta = (senderId : string ) => {
244
+ return props .partners .find ((partner ) => isStringEqualCI (partner .id , senderId ))
245
+ }
246
+
247
+ onMounted (() => {
248
+ attachScrollListener ()
249
+
250
+ if (messagesRef .value ) {
251
+ currentClientHeight .value = messagesRef .value .clientHeight
252
+ resizeObserver .value .observe (messagesRef .value )
253
+ }
254
+ })
255
+
256
+ onBeforeUnmount (() => {
257
+ destroyScrollListener ()
258
+ if (messagesRef .value ) {
259
+ resizeObserver .value ?.unobserve (messagesRef .value )
260
+ }
261
+ })
262
+
263
+ return {
264
+ classes ,
265
+ messagesRef ,
266
+ maintainScrollPosition ,
267
+ scrollToBottom ,
268
+ scrollTo ,
269
+ scrollToMessage ,
270
+ scrollToMessageEasy ,
271
+ getSenderMeta ,
272
+ isScrolledToBottom
237
273
}
238
274
}
239
- }
275
+ })
240
276
</script >
0 commit comments