Merge branch 'master' into chat-notifications2

This commit is contained in:
Ulf Gebhardt 2023-07-17 22:29:03 +02:00
commit 2e7adb4eb3
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
14 changed files with 403 additions and 133 deletions

View File

@ -6,6 +6,10 @@ export const createMessageMutation = () => {
CreateMessage(roomId: $roomId, content: $content) {
id
content
senderId
username
avatar
date
saved
distributed
seen
@ -17,7 +21,7 @@ export const createMessageMutation = () => {
export const messageQuery = () => {
return gql`
query ($roomId: ID!, $first: Int, $offset: Int) {
Message(roomId: $roomId, first: $first, offset: $offset, orderBy: createdAt_desc) {
Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) {
_id
id
indexId

View File

@ -6,6 +6,17 @@ export const createRoomMutation = () => {
CreateRoom(userId: $userId) {
id
roomId
roomName
lastMessageAt
unreadCount
users {
_id
id
name
avatar {
url
}
}
}
}
`
@ -18,6 +29,20 @@ export const roomQuery = () => {
id
roomId
roomName
lastMessageAt
unreadCount
lastMessage {
_id
id
content
senderId
username
avatar
date
saved
distributed
seen
}
users {
_id
id

View File

@ -0,0 +1,57 @@
import { isArray } from 'lodash'
const setRoomProps = (room) => {
if (room.users) {
room.users.forEach((user) => {
user._id = user.id
})
}
if (room.lastMessage) {
room.lastMessage._id = room.lastMessage.id
}
}
const setMessageProps = (message, context) => {
message._id = message.id
if (message.senderId !== context.user.id) {
message.distributed = true
}
}
const roomProperties = async (resolve, root, args, context, info) => {
const resolved = await resolve(root, args, context, info)
if (resolved) {
if (isArray(resolved)) {
resolved.forEach((room) => {
setRoomProps(room)
})
} else {
setRoomProps(resolved)
}
}
return resolved
}
const messageProperties = async (resolve, root, args, context, info) => {
const resolved = await resolve(root, args, context, info)
if (resolved) {
if (isArray(resolved)) {
resolved.forEach((message) => {
setMessageProps(message, context)
})
} else {
setMessageProps(resolved, context)
}
}
return resolved
}
export default {
Query: {
Room: roomProperties,
Message: messageProperties,
},
Mutation: {
CreateRoom: roomProperties,
},
}

View File

@ -14,6 +14,7 @@ import login from './login/loginMiddleware'
import sentry from './sentryMiddleware'
import languages from './languages/languages'
import userInteractions from './userInteractions'
import chatMiddleware from './chatMiddleware'
export default (schema) => {
const middlewares = {
@ -31,6 +32,7 @@ export default (schema) => {
orderBy,
languages,
userInteractions,
chatMiddleware,
}
let order = [
@ -49,6 +51,7 @@ export default (schema) => {
'softDelete',
'includedFields',
'orderBy',
'chatMiddleware',
]
// add permisions middleware at the first position (unless we're seeding)

View File

@ -1,13 +1,15 @@
import { createTestClient } from 'apollo-server-testing'
import Factory, { cleanDatabase } from '../../db/factories'
import { getNeode, getDriver } from '../../db/neo4j'
import { createRoomMutation } from '../../graphql/rooms'
import { createRoomMutation, roomQuery } from '../../graphql/rooms'
import { createMessageMutation, messageQuery, markMessagesAsSeen } from '../../graphql/messages'
import createServer from '../../server'
import createServer, { pubsub } from '../../server'
const driver = getDriver()
const neode = getNeode()
const pubsubSpy = jest.spyOn(pubsub, 'publish')
let query
let mutate
let authenticatedUser
@ -22,6 +24,9 @@ beforeAll(async () => {
driver,
neode,
user: authenticatedUser,
cypherParams: {
currentUserId: authenticatedUser ? authenticatedUser.id : null,
},
}
},
})
@ -55,6 +60,10 @@ describe('Message', () => {
})
describe('create message', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
await expect(
@ -77,7 +86,7 @@ describe('Message', () => {
})
describe('room does not exist', () => {
it('returns null', async () => {
it('returns null and does not publish subscription', async () => {
await expect(
mutate({
mutation: createMessageMutation(),
@ -92,6 +101,7 @@ describe('Message', () => {
CreateMessage: null,
},
})
expect(pubsubSpy).not.toBeCalled()
})
})
@ -107,7 +117,7 @@ describe('Message', () => {
})
describe('user chats in room', () => {
it('returns the message', async () => {
it('returns the message and publishes subscription', async () => {
await expect(
mutate({
mutation: createMessageMutation(),
@ -122,12 +132,78 @@ describe('Message', () => {
CreateMessage: {
id: expect.any(String),
content: 'Some nice message to other chatting user',
senderId: 'chatting-user',
username: 'Chatting User',
avatar: expect.any(String),
date: expect.any(String),
saved: true,
distributed: false,
seen: false,
},
},
})
expect(pubsubSpy).toBeCalledWith('ROOM_COUNT_UPDATED', {
roomCountUpdated: '1',
userId: 'other-chatting-user',
})
})
describe('room is updated as well', () => {
it('has last message set', async () => {
const result = await query({ query: roomQuery() })
await expect(result).toMatchObject({
errors: undefined,
data: {
Room: [
expect.objectContaining({
lastMessageAt: expect.any(String),
unreadCount: 0,
lastMessage: expect.objectContaining({
_id: result.data.Room[0].lastMessage.id,
id: expect.any(String),
content: 'Some nice message to other chatting user',
senderId: 'chatting-user',
username: 'Chatting User',
avatar: expect.any(String),
date: expect.any(String),
saved: true,
distributed: false,
seen: false,
}),
}),
],
},
})
})
})
describe('unread count for other user', () => {
it('has unread count = 1', async () => {
authenticatedUser = await otherChattingUser.toJson()
await expect(query({ query: roomQuery() })).resolves.toMatchObject({
errors: undefined,
data: {
Room: [
expect.objectContaining({
lastMessageAt: expect.any(String),
unreadCount: 1,
lastMessage: expect.objectContaining({
_id: expect.any(String),
id: expect.any(String),
content: 'Some nice message to other chatting user',
senderId: 'chatting-user',
username: 'Chatting User',
avatar: expect.any(String),
date: expect.any(String),
saved: true,
distributed: false,
seen: false,
}),
}),
],
},
})
})
})
})

View File

@ -1,9 +1,25 @@
import { neo4jgraphql } from 'neo4j-graphql-js'
import Resolver from './helpers/Resolver'
import RoomResolver from './rooms'
import { getUnreadRoomsCount } from './rooms'
import { pubsub, ROOM_COUNT_UPDATED, CHAT_MESSAGE_ADDED } from '../../server'
import { withFilter } from 'graphql-subscriptions'
const setMessagesAsDistributed = async (undistributedMessagesIds, session) => {
return session.writeTransaction(async (transaction) => {
const setDistributedCypher = `
MATCH (m:Message) WHERE m.id IN $undistributedMessagesIds
SET m.distributed = true
RETURN m { .* }
`
const setDistributedTxResponse = await transaction.run(setDistributedCypher, {
undistributedMessagesIds,
})
const messages = await setDistributedTxResponse.records.map((record) => record.get('m'))
return messages
})
}
export default {
Subscription: {
chatMessageAdded: {
@ -34,33 +50,15 @@ export default {
const undistributedMessagesIds = resolved
.filter((msg) => !msg.distributed && msg.senderId !== context.user.id)
.map((msg) => msg.id)
if (undistributedMessagesIds.length > 0) {
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const setDistributedCypher = `
MATCH (m:Message) WHERE m.id IN $undistributedMessagesIds
SET m.distributed = true
RETURN m { .* }
`
const setDistributedTxResponse = await transaction.run(setDistributedCypher, {
undistributedMessagesIds,
})
const messages = await setDistributedTxResponse.records.map((record) => record.get('m'))
return messages
})
try {
await writeTxResultPromise
} finally {
session.close()
const session = context.driver.session()
try {
if (undistributedMessagesIds.length > 0) {
await setMessagesAsDistributed(undistributedMessagesIds, session)
}
// send subscription to author to updated the messages
} finally {
session.close()
}
resolved.forEach((message) => {
message._id = message.id
if (message.senderId !== context.user.id) {
message.distributed = true
}
})
// send subscription to author to updated the messages
}
return resolved.reverse()
},
@ -75,8 +73,11 @@ export default {
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const createMessageCypher = `
MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId })
OPTIONAL MATCH (m:Message)-[:INSIDE]->(room)<-[:CHATS_IN]-(otherUser:User)
WITH MAX(m.indexId) as maxIndex, room, currentUser, otherUser
OPTIONAL MATCH (currentUser)-[:AVATAR_IMAGE]->(image:Image)
OPTIONAL MATCH (m:Message)-[:INSIDE]->(room)
OPTIONAL MATCH (room)<-[:CHATS_IN]-(recipientUser:User)
WHERE NOT recipientUser.id = $currentUserId
WITH MAX(m.indexId) as maxIndex, room, currentUser, image, recipientUser
CREATE (currentUser)-[:CREATED]->(message:Message {
createdAt: toString(datetime()),
id: apoc.create.uuid(),
@ -86,7 +87,15 @@ export default {
distributed: false,
seen: false
})-[:INSIDE]->(room)
RETURN message { .*, room: properties(room), senderId: currentUser.id, otherUser: properties(otherUser) }
SET room.lastMessageAt = toString(datetime())
RETURN message {
.*,
recipientId: recipientUser.id,
senderId: currentUser.id,
username: currentUser.name,
avatar: image.url,
date: message.createdAt
}
`
const createMessageTxResponse = await transaction.run(createMessageCypher, {
currentUserId,
@ -98,20 +107,24 @@ export default {
record.get('message'),
)
// TODO change user in context - mark message as seen - chattingUser is the correct user.
const roomCountUpdated = await RoomResolver.Query.UnreadRooms(null, null, context, null)
// send subscriptions
await pubsub.publish(ROOM_COUNT_UPDATED, { roomCountUpdated, user: message.otherUser })
await pubsub.publish(CHAT_MESSAGE_ADDED, {
chatMessageAdded: message,
user: message.otherUser,
})
return message
})
try {
const message = await writeTxResultPromise
if (message) {
const roomCountUpdated = await getUnreadRoomsCount(message.recipientId, session)
// send subscriptions
await pubsub.publish(ROOM_COUNT_UPDATED, {
roomCountUpdated,
userId: message.recipientId,
})
await pubsub.publish(CHAT_MESSAGE_ADDED, {
chatMessageAdded: message,
user: message.recipientId,
})
}
return message
} catch (error) {
throw new Error(error)

View File

@ -22,6 +22,9 @@ beforeAll(async () => {
driver,
neode,
user: authenticatedUser,
cypherParams: {
currentUserId: authenticatedUser ? authenticatedUser.id : null,
},
}
},
})
@ -131,6 +134,26 @@ describe('Room', () => {
CreateRoom: {
id: expect.any(String),
roomId: result.data.CreateRoom.id,
roomName: 'Other Chatting User',
unreadCount: 0,
users: expect.arrayContaining([
{
_id: 'chatting-user',
id: 'chatting-user',
name: 'Chatting User',
avatar: {
url: expect.any(String),
},
},
{
_id: 'other-chatting-user',
id: 'other-chatting-user',
name: 'Other Chatting User',
avatar: {
url: expect.any(String),
},
},
]),
},
},
})
@ -228,6 +251,7 @@ describe('Room', () => {
id: expect.any(String),
roomId: result.data.Room[0].id,
roomName: 'Chatting User',
unreadCount: 0,
users: expect.arrayContaining([
{
_id: 'chatting-user',

View File

@ -3,13 +3,25 @@ import Resolver from './helpers/Resolver'
import { pubsub, ROOM_COUNT_UPDATED } from '../../server'
import { withFilter } from 'graphql-subscriptions'
export const getUnreadRoomsCount = async (userId, session) => {
return session.readTransaction(async (transaction) => {
const unreadRoomsCypher = `
MATCH (:User { id: $userId })-[:CHATS_IN]->(room:Room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User)
WHERE NOT sender.id = $userId AND NOT message.seen
RETURN toString(COUNT(DISTINCT room)) AS count
`
const unreadRoomsTxResponse = await transaction.run(unreadRoomsCypher, { userId })
return unreadRoomsTxResponse.records.map((record) => record.get('count'))[0]
})
}
export default {
Subscription: {
roomCountUpdated: {
subscribe: withFilter(
() => pubsub.asyncIterator(ROOM_COUNT_UPDATED),
(payload, variables) => {
return payload.user.id === variables.userId
return payload.userId === variables.userId
},
),
},
@ -20,39 +32,15 @@ export default {
params.filter.users_some = {
id: context.user.id,
}
const resolved = await neo4jgraphql(object, params, context, resolveInfo)
if (resolved) {
resolved.forEach((room) => {
if (room.users) {
// buggy, you must query the username for this to function correctly
room.roomName = room.users.filter((user) => user.id !== context.user.id)[0].name
room.avatar =
room.users.filter((user) => user.id !== context.user.id)[0].avatar?.url ||
'default-avatar'
room.users.forEach((user) => {
user._id = user.id
})
}
})
}
return resolved
return neo4jgraphql(object, params, context, resolveInfo)
},
UnreadRooms: async (object, params, context, resolveInfo) => {
const {
user: { id: currentUserId },
} = context
const session = context.driver.session()
const readTxResultPromise = session.readTransaction(async (transaction) => {
const unreadRoomsCypher = `
MATCH (:User { id: $currentUserId })-[:CHATS_IN]->(room:Room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User)
WHERE NOT sender.id = $currentUserId AND NOT message.seen
RETURN toString(COUNT(DISTINCT room)) AS count
`
const unreadRoomsTxResponse = await transaction.run(unreadRoomsCypher, { currentUserId })
return unreadRoomsTxResponse.records.map((record) => record.get('count'))[0]
})
try {
const count = await readTxResultPromise
const count = await getUnreadRoomsCount(currentUserId, session)
return count
} finally {
session.close()
@ -77,7 +65,17 @@ export default {
ON CREATE SET
room.createdAt = toString(datetime()),
room.id = apoc.create.uuid()
RETURN room { .* }
WITH room, user, currentUser
OPTIONAL MATCH (room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User)
WHERE NOT sender.id = $currentUserId AND NOT message.seen
WITH room, user, currentUser, message,
user.name AS roomName
RETURN room {
.*,
users: [properties(currentUser), properties(user)],
roomName: roomName,
unreadCount: toString(COUNT(DISTINCT message))
}
`
const createRommTxResponse = await transaction.run(createRoomCypher, {
userId,
@ -101,6 +99,7 @@ export default {
},
Room: {
...Resolver('Room', {
undefinedToNull: ['lastMessageAt'],
hasMany: {
users: '<-[:CHATS_IN]-(related:User)',
},

View File

@ -3,8 +3,7 @@
# }
enum _MessageOrdering {
createdAt_asc
createdAt_desc
indexId_desc
}
type Message {

View File

@ -18,8 +18,28 @@ type Room {
users: [User]! @relation(name: "CHATS_IN", direction: "IN")
roomId: String! @cypher(statement: "RETURN this.id")
roomName: String! ## @cypher(statement: "MATCH (this)<-[:CHATS_IN]-(user:User) WHERE NOT user.id = $cypherParams.currentUserId RETURN user[0].name")
avatar: String! ## @cypher match not own user in users array
roomName: String! @cypher(statement: "MATCH (this)<-[:CHATS_IN]-(user:User) WHERE NOT user.id = $cypherParams.currentUserId RETURN user.name")
avatar: String @cypher(statement: """
MATCH (this)<-[:CHATS_IN]-(user:User)
WHERE NOT user.id = $cypherParams.currentUserId
OPTIONAL MATCH (user)-[:AVATAR_IMAGE]->(image:Image)
RETURN image.url
""")
lastMessageAt: String
lastMessage: Message @cypher(statement: """
MATCH (this)<-[:INSIDE]-(message:Message)
WITH message ORDER BY message.indexId DESC LIMIT 1
RETURN message
""")
unreadCount: Int @cypher(statement: """
MATCH (this)<-[:INSIDE]-(message:Message)<-[:CREATED]-(user:User)
WHERE NOT user.id = $cypherParams.currentUserId
AND NOT message.seen
RETURN count(message)
""")
}
type Mutation {

View File

@ -60,10 +60,10 @@
</template>
<script>
import { roomQuery, createRoom } from '~/graphql/Rooms'
import { messageQuery, createMessageMutation, chatMessageAdded } from '~/graphql/Messages'
import { roomQuery, createRoom, unreadRoomsQuery } from '~/graphql/Rooms'
import { messageQuery, createMessageMutation, chatMessageAdded, markMessagesAsSeen } from '~/graphql/Messages'
import chatStyle from '~/constants/chat.js'
import { mapGetters } from 'vuex'
import { mapGetters, mapMutations } from 'vuex'
export default {
name: 'Chat',
@ -84,31 +84,32 @@ export default {
name: 'dummyItem',
title: 'Just a dummy item',
},
/* {
name: 'inviteUser',
title: 'Invite User',
},
{
name: 'removeUser',
title: 'Remove User',
},
{
name: 'deleteRoom',
title: 'Delete Room',
},
*/
/*
{
name: 'inviteUser',
title: 'Invite User',
},
{
name: 'removeUser',
title: 'Remove User',
},
{
name: 'deleteRoom',
title: 'Delete Room',
},
*/
],
messageActions: [
/*
{
name: 'addMessageToFavorite',
title: 'Add To Favorite',
},
{
name: 'shareMessage',
title: 'Share Message',
},
*/
{
name: 'addMessageToFavorite',
title: 'Add To Favorite',
},
{
name: 'shareMessage',
title: 'Share Message',
},
*/
],
templatesText: [
{
@ -120,30 +121,16 @@ export default {
text: 'This is the action',
},
],
textMessages: {
ROOMS_EMPTY: this.$t('chat.roomsEmpty'),
ROOM_EMPTY: this.$t('chat.roomEmpty'),
NEW_MESSAGES: this.$t('chat.newMessages'),
MESSAGE_DELETED: this.$t('chat.messageDeleted'),
MESSAGES_EMPTY: this.$t('chat.messagesEmpty'),
CONVERSATION_STARTED: this.$t('chat.conversationStarted'),
TYPE_MESSAGE: this.$t('chat.typeMessage'),
SEARCH: this.$t('chat.search'),
IS_ONLINE: this.$t('chat.isOnline'),
LAST_SEEN: this.$t('chat.lastSeen'),
IS_TYPING: this.$t('chat.isTyping'),
CANCEL_SELECT_MESSAGE: this.$t('chat.cancelSelectMessage'),
},
roomActions: [
/*
{
name: 'archiveRoom',
title: 'Archive Room',
},
{ name: 'inviteUser', title: 'Invite User' },
{ name: 'removeUser', title: 'Remove User' },
{ name: 'deleteRoom', title: 'Delete Room' },
*/
{
name: 'archiveRoom',
title: 'Archive Room',
},
{ name: 'inviteUser', title: 'Invite User' },
{ name: 'removeUser', title: 'Remove User' },
{ name: 'deleteRoom', title: 'Delete Room' },
*/
],
showDemoOptions: true,
@ -151,7 +138,7 @@ export default {
rooms: [],
roomsLoaded: false,
roomPage: 0,
roomPageSize: 10, // TODO pagination is a problem with single rooms - cant use
roomPageSize: 10,
singleRoom: !!this.singleRoomId || false,
selectedRoom: null,
loadingRooms: true,
@ -206,8 +193,27 @@ export default {
computedChatStyle() {
return chatStyle.STYLE.light
},
textMessages() {
return {
ROOMS_EMPTY: this.$t('chat.roomsEmpty'),
ROOM_EMPTY: this.$t('chat.roomEmpty'),
NEW_MESSAGES: this.$t('chat.newMessages'),
MESSAGE_DELETED: this.$t('chat.messageDeleted'),
MESSAGES_EMPTY: this.$t('chat.messagesEmpty'),
CONVERSATION_STARTED: this.$t('chat.conversationStarted'),
TYPE_MESSAGE: this.$t('chat.typeMessage'),
SEARCH: this.$t('chat.search'),
IS_ONLINE: this.$t('chat.isOnline'),
LAST_SEEN: this.$t('chat.lastSeen'),
IS_TYPING: this.$t('chat.isTyping'),
CANCEL_SELECT_MESSAGE: this.$t('chat.cancelSelectMessage'),
}
},
},
methods: {
...mapMutations({
commitUnreadRoomCount: 'chat/UPDATE_ROOM_COUNT',
}),
async fetchRooms({ room } = {}) {
this.roomsLoaded = false
const offset = this.roomPage * this.roomPageSize
@ -268,8 +274,30 @@ export default {
fetchPolicy: 'no-cache',
})
const newMsgIds = Message.filter((m) => m.seen === false).map((m) => m.id)
if (newMsgIds.length) {
this.$apollo
.mutate({
mutation: markMessagesAsSeen(),
variables: {
messageIds: newMsgIds,
},
})
.then(() => {
this.$apollo
.query({
query: unreadRoomsQuery(),
fetchPolicy: 'network-only',
})
.then(({ data: { UnreadRooms } }) => {
this.commitUnreadRoomCount(UnreadRooms)
})
})
}
const msgs = []
;[...this.messages, ...Message].forEach((m) => {
if (m.senderId !== this.currentUser.id) m.seen = true
m.date = new Date(m.date).toDateString()
msgs[m.indexId] = m
})
@ -297,6 +325,12 @@ export default {
},
async sendMessage(message) {
// check for usersTag and change userid to username
message.usersTag.forEach((userTag) => {
const needle = `<usertag>${userTag.id}</usertag>`
const replacement = `<usertag>@${userTag.name.replaceAll(' ', '-').toLowerCase()}</usertag>`
message.content = message.content.replaceAll(needle, replacement)
})
try {
await this.$apollo.mutate({
mutation: createMessageMutation(),

View File

@ -8,13 +8,13 @@
placement: 'bottom-start',
}"
>
<counter-icon icon="chat-bubble" :count="count" danger />
<counter-icon icon="chat-bubble" :count="unreadRoomCount" danger />
</base-button>
</nuxt-link>
</template>
<script>
import { mapGetters } from 'vuex'
import { mapGetters, mapMutations } from 'vuex'
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
import { unreadRoomsQuery, roomCountUpdated } from '~/graphql/Rooms'
@ -23,14 +23,15 @@ export default {
components: {
CounterIcon,
},
data() {
return {
count: 0,
}
},
computed: {
...mapGetters({
user: 'auth/user',
unreadRoomCount: 'chat/unreadRoomCount',
}),
},
methods: {
...mapMutations({
commitUnreadRoomCount: 'chat/UPDATE_ROOM_COUNT',
}),
},
apollo: {
@ -39,7 +40,7 @@ export default {
return unreadRoomsQuery()
},
update({ UnreadRooms }) {
this.count = UnreadRooms
this.commitUnreadRoomCount(UnreadRooms)
},
subscribeToMore: {
document: roomCountUpdated(),

View File

@ -3,7 +3,7 @@ import gql from 'graphql-tag'
export const messageQuery = () => {
return gql`
query ($roomId: ID!, $first: Int, $offset: Int) {
Message(roomId: $roomId, first: $first, offset: $offset, orderBy: createdAt_desc) {
Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) {
_id
id
indexId
@ -59,3 +59,11 @@ export const chatMessageAdded = () => {
}
`
}
export const markMessagesAsSeen = () => {
return gql`
mutation ($messageIds: [String!]) {
MarkMessagesAsSeen(messageIds: $messageIds)
}
`
}

View File

@ -2,6 +2,7 @@ export const state = () => {
return {
showChat: false,
roomID: null,
unreadRoomCount: 0,
}
}
@ -10,6 +11,9 @@ export const mutations = {
state.showChat = ctx.showChat || false
state.roomID = ctx.roomID || null
},
UPDATE_ROOM_COUNT(state, count) {
state.unreadRoomCount = count
},
}
export const getters = {
@ -19,4 +23,7 @@ export const getters = {
roomID(state) {
return state
},
unreadRoomCount(state) {
return state.unreadRoomCount
},
}