From d73fd0cbb75d7080f3c55612e727e62e78b6ef8e Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 13 Jul 2023 22:35:48 +0200 Subject: [PATCH 01/29] [bug] chat language is now reactive --- webapp/components/Chat/Chat.vue | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/webapp/components/Chat/Chat.vue b/webapp/components/Chat/Chat.vue index 8aeb4e7de..855f1e14b 100644 --- a/webapp/components/Chat/Chat.vue +++ b/webapp/components/Chat/Chat.vue @@ -114,20 +114,6 @@ 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: [ /* { @@ -177,6 +163,22 @@ export default { // return this.theme === 'light' ? chatStyle.STYLE.light : chatStyle.STYLE.dark 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: { fetchMessages({ room, options = {} }) { From d496aef0a42abb9fdd17a7c8ac95c2653c0172b8 Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 13 Jul 2023 22:56:38 +0200 Subject: [PATCH 02/29] fixed linting --- webapp/components/Chat/Chat.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/components/Chat/Chat.vue b/webapp/components/Chat/Chat.vue index 855f1e14b..eb7d7124a 100644 --- a/webapp/components/Chat/Chat.vue +++ b/webapp/components/Chat/Chat.vue @@ -164,7 +164,7 @@ export default { return chatStyle.STYLE.light }, textMessages() { - return{ + return { ROOMS_EMPTY: this.$t('chat.roomsEmpty'), ROOM_EMPTY: this.$t('chat.roomEmpty'), NEW_MESSAGES: this.$t('chat.newMessages'), @@ -178,7 +178,7 @@ export default { IS_TYPING: this.$t('chat.isTyping'), CANCEL_SELECT_MESSAGE: this.$t('chat.cancelSelectMessage'), } - } + }, }, methods: { fetchMessages({ room, options = {} }) { From f9da7622538d0d73a974ad91b999eba26da8d357 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 14 Jul 2023 10:42:20 +0200 Subject: [PATCH 03/29] implemented chat seed --- backend/src/db/seed.ts | 86 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts index a717ff7a6..425c9e28c 100644 --- a/backend/src/db/seed.ts +++ b/backend/src/db/seed.ts @@ -11,6 +11,8 @@ import { changeGroupMemberRoleMutation, } from '../graphql/groups' import { createPostMutation } from '../graphql/posts' +import { createRoomMutation } from '../graphql/rooms' +import { createMessageMutation } from '../graphql/messages' import { createCommentMutation } from '../graphql/comments' import { categories } from '../constants/categories' @@ -1553,6 +1555,90 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] ) await Factory.build('donations') + + // Chat + authenticatedUser = await huey.toJson() + const { data: roomHueyPeter } = await mutate({ + mutation: createRoomMutation(), + variables: { + userId: (await peterLustig.toJson()).id, + }, + }) + + for (let i = 0; i < 30; i++) { + authenticatedUser = await huey.toJson() + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: roomHueyPeter?.CreateRoom.id, + content: faker.lorem.sentence(), + }, + }) + authenticatedUser = await peterLustig.toJson() + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: roomHueyPeter?.CreateRoom.id, + content: faker.lorem.sentence(), + }, + }) + } + + authenticatedUser = await huey.toJson() + const { data: roomHueyJenny } = await mutate({ + mutation: createRoomMutation(), + variables: { + userId: (await jennyRostock.toJson()).id, + }, + }) + for (let i = 0; i < 10000; i++) { + authenticatedUser = await huey.toJson() + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: roomHueyJenny?.CreateRoom.id, + content: faker.lorem.sentence(), + }, + }) + authenticatedUser = await jennyRostock.toJson() + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: roomHueyJenny?.CreateRoom.id, + content: faker.lorem.sentence(), + }, + }) + } + + for (const user of additionalUsers) { + authenticatedUser = await jennyRostock.toJson() + const { data: room } = await mutate({ + mutation: createRoomMutation(), + variables: { + userId: (await user.toJson()).id, + }, + }) + + for (let i = 0; i < 30; i++) { + authenticatedUser = await jennyRostock.toJson() + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: room?.CreateRoom.id, + content: faker.lorem.sentence(), + }, + }) + authenticatedUser = await user.toJson() + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId: room?.CreateRoom.id, + content: faker.lorem.sentence(), + }, + }) + } + } + /* eslint-disable-next-line no-console */ console.log('Seeded Data...') await driver.close() From eae7b53adf79de8e87ee1fa1acf050de9e36b540 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 14 Jul 2023 10:56:02 +0200 Subject: [PATCH 04/29] reduce message count to 1000 instead of 10000 to reduce seed time --- backend/src/db/seed.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts index 425c9e28c..cf3f43ee9 100644 --- a/backend/src/db/seed.ts +++ b/backend/src/db/seed.ts @@ -1591,7 +1591,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] userId: (await jennyRostock.toJson()).id, }, }) - for (let i = 0; i < 10000; i++) { + for (let i = 0; i < 1000; i++) { authenticatedUser = await huey.toJson() await mutate({ mutation: createMessageMutation(), From 55b3bc999a57588e90ba90fba71052b83d919e3e Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 14 Jul 2023 10:59:38 +0200 Subject: [PATCH 05/29] use a prime for seeding to allow testing of the pagination --- backend/src/db/seed.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts index cf3f43ee9..7286683dd 100644 --- a/backend/src/db/seed.ts +++ b/backend/src/db/seed.ts @@ -1619,7 +1619,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] }, }) - for (let i = 0; i < 30; i++) { + for (let i = 0; i < 29; i++) { authenticatedUser = await jennyRostock.toJson() await mutate({ mutation: createMessageMutation(), From f4567b14ff838a5f78e2f132d471d8ce4aa7da0a Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 14 Jul 2023 13:25:57 +0200 Subject: [PATCH 06/29] 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 07/29] 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 08/29] 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 09/29] 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 10/29] 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', - }, - }, }