From f4567b14ff838a5f78e2f132d471d8ce4aa7da0a Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 14 Jul 2023 13:25:57 +0200 Subject: [PATCH 01/23] feat(backend): unread rooms query --- backend/src/schema/resolvers/rooms.ts | 21 +++++++++++++++++++++ backend/src/schema/types/type/Room.gql | 1 + 2 files changed, 22 insertions(+) diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts index d5015a03b..3eda1440c 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/schema/resolvers/rooms.ts @@ -25,6 +25,27 @@ export default { } return resolved }, + 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]-(user:User) + WHERE NOT message.seen AND NOT user.id = $currentUserId + RETURN toString(COUNT(room)) AS count + ` + const unreadRoomsTxResponse = await transaction.run(unreadRoomsCypher, { currentUserId }) + return unreadRoomsTxResponse.records.map((record) => record.get('count')) + }) + try { + const count = await readTxResultPromise + return count + } finally { + session.close() + } + }, }, Mutation: { CreateRoom: async (_parent, params, context, _resolveInfo) => { diff --git a/backend/src/schema/types/type/Room.gql b/backend/src/schema/types/type/Room.gql index 2ce6556f6..55984fb5f 100644 --- a/backend/src/schema/types/type/Room.gql +++ b/backend/src/schema/types/type/Room.gql @@ -25,4 +25,5 @@ type Mutation { type Query { Room: [Room] + UnreadRooms: Int } From 09be4d3442e227bb7722cfc5fe0b4a3d9445a44c Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 14 Jul 2023 14:40:38 +0200 Subject: [PATCH 02/23] test unread rooms query --- backend/src/graphql/rooms.ts | 8 ++ .../src/middleware/permissionsMiddleware.ts | 1 + backend/src/schema/resolvers/rooms.spec.ts | 120 +++++++++++++++++- backend/src/schema/resolvers/rooms.ts | 6 +- 4 files changed, 128 insertions(+), 7 deletions(-) diff --git a/backend/src/graphql/rooms.ts b/backend/src/graphql/rooms.ts index 109bf1d55..cb511c4eb 100644 --- a/backend/src/graphql/rooms.ts +++ b/backend/src/graphql/rooms.ts @@ -30,3 +30,11 @@ export const roomQuery = () => { } ` } + +export const unreadRoomsQuery = () => { + return gql` + query { + UnreadRooms + } + ` +} diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index c07098a3c..f87f4b079 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -408,6 +408,7 @@ export default shield( getInviteCode: isAuthenticated, // and inviteRegistration Room: isAuthenticated, Message: isAuthenticated, + UnreadRooms: isAuthenticated, }, Mutation: { '*': deny, diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts index 03c3d4456..6a8cb47de 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -1,7 +1,8 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' import { getNeode, getDriver } from '../../db/neo4j' -import { createRoomMutation, roomQuery } from '../../graphql/rooms' +import { createRoomMutation, roomQuery, unreadRoomsQuery } from '../../graphql/rooms' +import { createMessageMutation } from '../../graphql/messages' import createServer from '../../server' const driver = getDriver() @@ -29,11 +30,13 @@ beforeAll(async () => { }) afterAll(async () => { - await cleanDatabase() + // await cleanDatabase() driver.close() }) describe('Room', () => { + let roomId: string + beforeAll(async () => { ;[chattingUser, otherChattingUser, notChattingUser] = await Promise.all([ Factory.build('user', { @@ -68,8 +71,6 @@ describe('Room', () => { }) describe('authenticated', () => { - let roomId: string - beforeAll(async () => { authenticatedUser = await chattingUser.toJson() }) @@ -260,4 +261,115 @@ describe('Room', () => { }) }) }) + + describe('unread rooms query', () => { + describe('unauthenticated', () => { + it('throws authorization error', async () => { + authenticatedUser = null + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated', () => { + let otherRoomId: string + + beforeAll(async () => { + authenticatedUser = await chattingUser.toJson() + const result = await mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'not-chatting-user', + }, + }) + otherRoomId = result.data.CreateRoom.roomId + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: otherRoomId, + content: 'Message to not chatting user', + }, + }) + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: '1st message to other chatting user', + }, + }) + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: '2nd message to other chatting user', + }, + }) + authenticatedUser = await otherChattingUser.toJson() + const result2 = await mutate({ + mutation: createRoomMutation(), + variables: { + userId: 'not-chatting-user', + }, + }) + otherRoomId = result2.data.CreateRoom.roomId + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: otherRoomId, + content: 'Other message to not chatting user', + }, + }) + }) + + describe('as chatting user', () => { + it('has 0 unread rooms', async () => { + authenticatedUser = await chattingUser.toJson() + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + data: { + UnreadRooms: 0, + }, + }) + }) + }) + + describe('as other chatting user', () => { + it('has 1 unread rooms', async () => { + authenticatedUser = await otherChattingUser.toJson() + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + data: { + UnreadRooms: 1, + }, + }) + }) + }) + + describe('as not chatting user', () => { + it('has 2 unread rooms', async () => { + authenticatedUser = await notChattingUser.toJson() + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + data: { + UnreadRooms: 2, + }, + }) + }) + }) + }) + }) }) diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts index 3eda1440c..084a75eb0 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/schema/resolvers/rooms.ts @@ -33,11 +33,11 @@ export default { const readTxResultPromise = session.readTransaction(async (transaction) => { const unreadRoomsCypher = ` MATCH (:User { id: $currentUserId })-[:CHATS_IN]->(room:Room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(user:User) - WHERE NOT message.seen AND NOT user.id = $currentUserId - RETURN toString(COUNT(room)) AS count + WHERE NOT user.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')) + return unreadRoomsTxResponse.records.map((record) => record.get('count'))[0] }) try { const count = await readTxResultPromise From d7746e5904a44460404fa5ebf6d554ef42d17882 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 14 Jul 2023 14:44:41 +0200 Subject: [PATCH 03/23] clean db after all --- backend/src/schema/resolvers/rooms.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts index 6a8cb47de..8c46794c7 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -30,7 +30,7 @@ beforeAll(async () => { }) afterAll(async () => { - // await cleanDatabase() + await cleanDatabase() driver.close() }) From c46d0064fcd33b175fa71e95aab747bef587aa04 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 14 Jul 2023 14:50:43 +0200 Subject: [PATCH 04/23] unread rooms query in chat notification --- .../ChatNotificationMenu.vue | 18 +++++++++++++++++- webapp/graphql/Rooms.js | 8 ++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/webapp/components/ChatNotificationMenu/ChatNotificationMenu.vue b/webapp/components/ChatNotificationMenu/ChatNotificationMenu.vue index fcd93ee48..016410216 100644 --- a/webapp/components/ChatNotificationMenu/ChatNotificationMenu.vue +++ b/webapp/components/ChatNotificationMenu/ChatNotificationMenu.vue @@ -8,18 +8,34 @@ placement: 'bottom-start', }" > - + diff --git a/webapp/graphql/Rooms.js b/webapp/graphql/Rooms.js index e28702f77..c659ba85c 100644 --- a/webapp/graphql/Rooms.js +++ b/webapp/graphql/Rooms.js @@ -27,3 +27,11 @@ export const createRoom = () => gql` } } ` + +export const unreadRoomsQuery = () => { + return gql` + query { + UnreadRooms + } + ` +} From f05a0bc5448bbd55116283d928b82d1381955412 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Sat, 15 Jul 2023 10:38:19 +0200 Subject: [PATCH 05/23] frontend paginate rooms --- webapp/components/Chat/Chat.vue | 108 +++++++++++++++++--------------- webapp/graphql/Rooms.js | 4 +- 2 files changed, 60 insertions(+), 52 deletions(-) diff --git a/webapp/components/Chat/Chat.vue b/webapp/components/Chat/Chat.vue index 95bf5da95..437f8bcf1 100644 --- a/webapp/components/Chat/Chat.vue +++ b/webapp/components/Chat/Chat.vue @@ -13,13 +13,15 @@ :messages-loaded="messagesLoaded" :rooms="JSON.stringify(rooms)" :room-actions="JSON.stringify(roomActions)" - :rooms-loaded="true" + :rooms-loaded="roomsLoaded" + :loading-rooms="loadingRooms" show-files="false" show-audio="false" :styles="JSON.stringify(computedChatStyle)" :show-footer="true" @send-message="sendMessage($event.detail[0])" @fetch-messages="fetchMessages($event.detail[0])" + @fetch-more-rooms="fetchRooms" :responsive-breakpoint="responsiveBreakpoint" :single-room="singleRoom" show-reaction-emojis="false" @@ -143,17 +145,20 @@ export default { { name: 'deleteRoom', title: 'Delete Room' }, */ ], - rooms: [], - messages: [], - messagesLoaded: true, + showDemoOptions: true, responsiveBreakpoint: 600, + rooms: [], + roomsLoaded: false, + roomPage: 0, + roomPageSize: 10, // TODO pagination is a problem with single rooms - cant use singleRoom: !!this.singleRoomId || false, + selectedRoom: null, + loadingRooms: true, + messagesLoaded: false, messagePage: 0, messagePageSize: 20, - roomPage: 0, - roomPageSize: 999, // TODO pagination is a problem with single rooms - cant use - selectedRoom: null, + messages: [], } }, mounted() { @@ -165,8 +170,8 @@ export default { userId: this.singleRoomId, }, }) - .then(() => { - this.$apollo.queries.Rooms.refetch() + .then(({data: { CreateRoom }}) => { + this.fetchRooms({room: CreateRoom}) }) .catch((error) => { this.$toast.error(error) @@ -174,6 +179,8 @@ export default { .finally(() => { // this.loading = false }) + } else { + this.fetchRooms() } }, computed: { @@ -181,12 +188,52 @@ export default { currentUser: 'auth/user', }), computedChatStyle() { - // TODO light/dark theme still needed? - // return this.theme === 'light' ? chatStyle.STYLE.light : chatStyle.STYLE.dark return chatStyle.STYLE.light }, }, methods: { + async fetchRooms({ room } = {}){ + console.log(room) + this.roomsLoaded = false + const offset = this.roomPage * this.roomPageSize + try { + const { + data: { Room }, + } = await this.$apollo.query({ + query: roomQuery(), + variables: { + id: room?.id, + first: this.roomPageSize, + offset, + }, + fetchPolicy: 'no-cache', + }) + + console.log(Room) + const rooms = [] + const newRooms = Room.map((r) => { + return { + ...r, + users: r.users.map((u) => { + return { ...u, username: u.name, avatar: u.avatar?.url } + }), + } + }) + + this.rooms = [...this.rooms, ...newRooms] + + if (Room.length < this.roomPageSize) { + this.roomsLoaded = true + } + this.roomPage += 1 + } catch (error) { + this.rooms = [] + this.$toast.error(error.message) + } + // must be set false after initial rooms are loaded and never changed again + this.loadingRooms = false + }, + async fetchMessages({ room, options = {} }) { if (this.selectedRoom !== room.id) { this.messages = [] @@ -251,45 +298,6 @@ export default { return fullname.match(/\b\w/g).join('').substring(0, 3).toUpperCase() }, }, - apollo: { - Rooms: { - query() { - return roomQuery() - }, - variables() { - return { - first: this.roomPageSize, - offset: this.roomPage * this.roomPageSize, - } - }, - update({ Room }) { - if (!Room) { - this.rooms = [] - return - } - - // Backend result needs mapping of the following values - // room[i].users[j].name -> room[i].users[j].username - // room[i].users[j].avatar.url -> room[i].users[j].avatar - // also filter rooms for the single room - this.rooms = Room.map((r) => { - return { - ...r, - users: r.users.map((u) => { - return { ...u, username: u.name, avatar: u.avatar?.url } - }), - } - }).filter((r) => - this.singleRoom ? r.users.filter((u) => u.id === this.singleRoomId).length > 0 : true, - ) - }, - error(error) { - this.rooms = [] - this.$toast.error(error.message) - }, - fetchPolicy: 'no-cache', - }, - }, }