Skip to content

Commit 29305f4

Browse files
committed
(feat) add online status and typing indicator
1 parent df0a86b commit 29305f4

File tree

7 files changed

+193
-24
lines changed

7 files changed

+193
-24
lines changed

demo/src/ChatContainer.vue

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,18 @@
6464
@menuActionHandler="menuActionHandler"
6565
@messageActionHandler="messageActionHandler"
6666
@sendMessageReaction="sendMessageReaction"
67+
@typingMessage="typingMessage"
6768
/>
6869
</div>
6970
</template>
7071

7172
<script>
7273
import {
74+
firebase,
7375
roomsRef,
7476
usersRef,
7577
filesRef,
76-
deleteDbField,
77-
firebase
78+
deleteDbField
7879
} from '@/firestore'
7980
import { parseTimestamp, isSameDay } from '@/utils/dates'
8081
import ChatWindow from './../../src/ChatWindow'
@@ -118,6 +119,7 @@ export default {
118119
119120
mounted() {
120121
this.fetchRooms()
122+
this.updateUserOnlineStatus()
121123
},
122124
123125
destroyed() {
@@ -147,14 +149,21 @@ export default {
147149
async fetchRooms() {
148150
this.resetRooms()
149151
150-
const rooms = await roomsRef
151-
.where('users', 'array-contains', this.currentUserId)
152-
.get()
152+
const query = roomsRef.where(
153+
'users',
154+
'array-contains',
155+
this.currentUserId
156+
)
153157
158+
const rooms = await query.get()
159+
160+
const roomList = []
154161
const rawRoomUsers = []
155162
const rawMessages = []
156163
157164
rooms.forEach(room => {
165+
roomList[room.id] = { ...room.data(), users: [] }
166+
158167
const rawUsers = []
159168
160169
room.data().users.map(userId => {
@@ -178,14 +187,11 @@ export default {
178187
rawMessages.push(this.getLastMessage(room))
179188
})
180189
181-
let roomList = []
182-
183190
const users = await Promise.all(rawRoomUsers)
184191
185-
users.map(user => {
186-
if (roomList[user.roomId]) roomList[user.roomId].users.push(user)
187-
else roomList[user.roomId] = { users: [user] }
188-
})
192+
users.map(user => roomList[user.roomId].users.push(user))
193+
194+
this.listenUsersOnlineStatus(users)
189195
190196
const roomMessages = await Promise.all(rawMessages).then(messages => {
191197
return messages.map(message => {
@@ -227,6 +233,8 @@ export default {
227233
this.rooms = this.rooms.concat(formattedRooms)
228234
this.loadingRooms = false
229235
this.rooms.map((room, index) => this.listenLastMessage(room, index))
236+
237+
this.listenRoomsTypingUsers(query)
230238
},
231239
232240
getLastMessage(room) {
@@ -261,7 +269,7 @@ export default {
261269
const timestampFormat = isSameDay(date, new Date()) ? 'HH:mm' : 'DD/MM/YY'
262270
263271
let timestamp = parseTimestamp(message.timestamp, timestampFormat)
264-
if (timestampFormat === 'HH:mm') timestamp = 'Today, ' + timestamp
272+
if (timestampFormat === 'HH:mm') timestamp = `Today, ${timestamp}`
265273
266274
let content = message.content
267275
if (message.file) content = `${message.file.name}.${message.file.type}`
@@ -465,6 +473,87 @@ export default {
465473
})
466474
},
467475
476+
typingMessage({ message, roomId }) {
477+
const dbAction = message
478+
? firebase.firestore.FieldValue.arrayUnion(this.currentUserId)
479+
: firebase.firestore.FieldValue.arrayRemove(this.currentUserId)
480+
481+
roomsRef.doc(roomId).update({
482+
typingUsers: dbAction
483+
})
484+
},
485+
486+
async listenRoomsTypingUsers(query) {
487+
query.onSnapshot(rooms => {
488+
rooms.forEach(room => {
489+
const foundRoom = this.rooms.find(r => r.roomId === room.id)
490+
if (foundRoom) foundRoom.typingUsers = room.data().typingUsers
491+
})
492+
})
493+
},
494+
495+
updateUserOnlineStatus() {
496+
const userStatusRef = firebase
497+
.database()
498+
.ref('/status/' + this.currentUserId)
499+
500+
const isOfflineData = {
501+
state: 'offline',
502+
last_changed: firebase.database.ServerValue.TIMESTAMP
503+
}
504+
505+
const isOnlineData = {
506+
state: 'online',
507+
last_changed: firebase.database.ServerValue.TIMESTAMP
508+
}
509+
510+
firebase
511+
.database()
512+
.ref('.info/connected')
513+
.on('value', snapshot => {
514+
if (snapshot.val() == false) return
515+
516+
userStatusRef
517+
.onDisconnect()
518+
.set(isOfflineData)
519+
.then(() => {
520+
userStatusRef.set(isOnlineData)
521+
})
522+
})
523+
},
524+
525+
listenUsersOnlineStatus(users) {
526+
users.map(user => {
527+
firebase
528+
.database()
529+
.ref('/status/' + user._id)
530+
.on('value', snapshot => {
531+
if (!snapshot.val()) return
532+
533+
const foundUser = users.find(u => snapshot.key === u._id)
534+
535+
if (foundUser) {
536+
const timestampFormat = isSameDay(
537+
new Date(snapshot.val().last_changed),
538+
new Date()
539+
)
540+
? 'HH:mm'
541+
: 'DD MMMM, HH:mm'
542+
543+
const timestamp = parseTimestamp(
544+
new Date(snapshot.val().last_changed),
545+
timestampFormat
546+
)
547+
548+
const last_changed =
549+
timestampFormat === 'HH:mm' ? `today, ${timestamp}` : timestamp
550+
551+
foundUser.status = { ...snapshot.val(), last_changed }
552+
}
553+
})
554+
})
555+
},
556+
468557
addRoom() {
469558
this.resetForms()
470559
this.addNewRoom = true

demo/src/firestore/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as app from 'firebase/app'
22
import 'firebase/firestore'
3+
import 'firebase/database'
34
import 'firebase/storage'
45

56
const config =

demo/src/utils/dates.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ export const parseTimestamp = (timestamp, format = '') => {
1313
} else if (format === 'DD/MM/YY') {
1414
const options = { month: 'numeric', year: 'numeric', day: 'numeric' }
1515
return `${new Intl.DateTimeFormat('en-GB', options).format(date)}`
16+
} else if (format === 'DD MMMM, HH:mm') {
17+
const options = { month: 'long', day: 'numeric' }
18+
return `${new Intl.DateTimeFormat('en-GB', options).format(
19+
date
20+
)}, ${zeroPad(date.getHours(), 2)}:${zeroPad(date.getMinutes(), 2)}`
1621
}
22+
1723
return date
1824
}
1925

src/ChatWindow/ChatWindow.vue

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
@menuActionHandler="menuActionHandler"
3737
@messageActionHandler="messageActionHandler"
3838
@sendMessageReaction="sendMessageReaction"
39+
@typingMessage="typingMessage"
3940
>
4041
</room>
4142
</div>
@@ -92,8 +93,10 @@ export default {
9293
},
9394
9495
watch: {
95-
rooms(val) {
96-
if (val[0]) this.fetchRoom({ room: val[0] })
96+
rooms(newVal, oldVal) {
97+
if (newVal[0] && newVal.length !== oldVal.length) {
98+
this.fetchRoom({ room: newVal[0] })
99+
}
97100
},
98101
99102
room(val) {
@@ -190,6 +193,12 @@ export default {
190193
...messageReaction,
191194
roomId: this.room.roomId
192195
})
196+
},
197+
typingMessage(message) {
198+
this.$emit('typingMessage', {
199+
message,
200+
roomId: this.room.roomId
201+
})
193202
}
194203
}
195204
}

src/ChatWindow/Room.vue

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,15 @@
1717
class="room-avatar"
1818
:style="{ 'background-image': `url(${room.avatar})` }"
1919
></div>
20-
<div class="room-name">{{ room.roomName }}</div>
20+
<div>
21+
<div class="room-name">{{ room.roomName }}</div>
22+
<div v-if="typingUsers" class="room-info">
23+
{{ typingUsers }} {{ textMessages.IS_TYPING }}
24+
</div>
25+
<div v-else-if="userStatus" class="room-info">
26+
{{ userStatus }}
27+
</div>
28+
</div>
2129
<div
2230
class="svg-button room-options"
2331
v-if="menuActions.length"
@@ -63,7 +71,7 @@
6371
</infinite-loading>
6472
</transition>
6573
<transition-group name="fade-message">
66-
<div v-for="(message, i) in messages" :key="message._id">
74+
<div v-for="(message, i) in messages" :key="i">
6775
<message
6876
:currentUserId="currentUserId"
6977
:message="message"
@@ -150,7 +158,7 @@
150158
'padding-left': `${imageDimensions.width + 6}px`
151159
}"
152160
v-model="message"
153-
@input="autoGrow"
161+
@input="onChangeInput"
154162
@keydown.esc="resetMessage"
155163
@keydown.enter.exact.prevent="sendMessage"
156164
></textarea>
@@ -279,9 +287,12 @@ export default {
279287
if (val) this.infiniteState = null
280288
else this.focusTextarea()
281289
},
282-
room() {
283-
this.loadingMessages = true
284-
this.resetMessage()
290+
room(newVal, oldVal) {
291+
if (newVal.roomId && newVal.roomId !== oldVal.roomId) {
292+
this.loadingMessages = true
293+
this.resetMessage()
294+
this.$emit('typingMessage', this.message)
295+
}
285296
},
286297
messages(newVal, oldVal) {
287298
newVal.forEach(message => {
@@ -340,6 +351,35 @@ export default {
340351
},
341352
inputDisabled() {
342353
return this.isMessageEmpty()
354+
},
355+
typingUsers() {
356+
if (!this.room.typingUsers || !this.room.typingUsers.length) return
357+
358+
const typingUsers = this.room.users.filter(user => {
359+
if (user._id === this.currentUserId) return
360+
if (this.room.typingUsers.indexOf(user._id) === -1) return
361+
if (user.status && user.status.state === 'offline') return
362+
return true
363+
})
364+
365+
return typingUsers.map(user => user.username).join(', ')
366+
},
367+
userStatus() {
368+
if (!this.room.users || this.room.users.length !== 2) return
369+
370+
const user = this.room.users.find(u => u._id !== this.currentUserId)
371+
372+
if (!user.status) return
373+
374+
let text = ''
375+
376+
if (user.status.state === 'online') {
377+
text = this.textMessages.IS_ONLINE
378+
} else if (user.status.last_changed) {
379+
text = this.textMessages.LAST_SEEN + user.status.last_changed
380+
}
381+
382+
return text
343383
}
344384
},
345385
@@ -435,6 +475,10 @@ export default {
435475
const element = this.$refs.scrollContainer
436476
element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' })
437477
},
478+
onChangeInput(el) {
479+
this.autoGrow(el)
480+
this.$emit('typingMessage', this.message)
481+
},
438482
autoGrow(el) {
439483
this.resizeTextarea(el.srcElement)
440484
},
@@ -527,6 +571,14 @@ export default {
527571
528572
.room-name {
529573
font-size: 17px;
574+
line-height: 18px;
575+
color: var(--chat-header-color-name);
576+
}
577+
578+
.room-info {
579+
font-size: 13px;
580+
line-height: 18px;
581+
color: var(--chat-header-color-info);
530582
}
531583
532584
.room-options {
@@ -546,7 +598,7 @@ export default {
546598
547599
.text-started {
548600
font-size: 14px;
549-
color: #9ca6af;
601+
color: var(--chat-message-color-started);
550602
font-style: italic;
551603
text-align: center;
552604
margin-top: 27px;

src/locales/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ export default {
44
MESSAGES_EMPTY: 'No messages',
55
CONVERSATION_STARTED: 'Conversation started on:',
66
TYPE_MESSAGE: 'Type message',
7-
SEARCH: 'Search'
7+
SEARCH: 'Search',
8+
IS_ONLINE: 'is online',
9+
LAST_SEEN: 'last seen ',
10+
IS_TYPING: 'is writing...'
811
}

0 commit comments

Comments
 (0)