From 4e81b1ed59e7276330718eb9b04b400d05a65a53 Mon Sep 17 00:00:00 2001 From: Dominik Halfkann Date: Thu, 28 Dec 2023 16:07:14 +0100 Subject: [PATCH 1/8] fetch recommendations after 1 hour --- .gitignore | 1 + .../data-access/recommendation.service.ts | 40 ++++++++++++------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index d01cf56b..efc7c0a2 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ speed-measure-plugin*.json *.launch .settings/ *.sublime-workspace +.fleet/ # IDE - VSCode .vscode/* diff --git a/src/app/recommendation/data-access/recommendation.service.ts b/src/app/recommendation/data-access/recommendation.service.ts index a6ca8100..bb030385 100644 --- a/src/app/recommendation/data-access/recommendation.service.ts +++ b/src/app/recommendation/data-access/recommendation.service.ts @@ -1,29 +1,39 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { RecommendationHttpService } from './recommendation-http.service'; import { RecommendedDancer } from './types/recommended-dancers.types'; -import { map, Observable, shareReplay } from 'rxjs'; +import { map, Observable, shareReplay, switchMap } from 'rxjs'; import { RecommendationsDto } from './types/recommendations.dto'; import { OldAPIResponse } from '@shared/util/http/response.types'; +import { TimerService } from '@shared/util/time/timer.service'; +import { startWith } from 'rxjs/operators'; @Injectable({ providedIn: 'root', }) export class RecommendationService { - constructor(private httpService: RecommendationHttpService) {} + private readonly httpService = inject(RecommendationHttpService); + private readonly timerService = inject(TimerService); - private readonly recommendations$ = this.httpService - .getRecommendations$() + constructor() {} + + private readonly recommendations$ = this.timerService + .interval('fetchRecommendations', 60 * 60 * 1000) // 60 minutes .pipe( - map((response) => { - if (response.isSuccess) { - return { - ...response, - payload: this.mapDtoToRecommendedDancers(response.payload), - }; - } - return response; - }), - shareReplay(1) + startWith(-1), + switchMap(() => + this.httpService.getRecommendations$().pipe( + map((response) => { + if (response.isSuccess) { + return { + ...response, + payload: this.mapDtoToRecommendedDancers(response.payload), + }; + } + return response; + }), + shareReplay(1) + ) + ) ); getRecommendations$(): Observable> { From 982ead674a311ee8cdac90e57334956e92218519 Mon Sep 17 00:00:00 2001 From: Dominik Halfkann Date: Thu, 28 Dec 2023 19:59:47 +0100 Subject: [PATCH 2/8] increase time between chat fetches when user is not on chat page --- src/app/shared/data-access/chat/unread-messages.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/shared/data-access/chat/unread-messages.service.ts b/src/app/shared/data-access/chat/unread-messages.service.ts index 607b8afb..b05eaf29 100644 --- a/src/app/shared/data-access/chat/unread-messages.service.ts +++ b/src/app/shared/data-access/chat/unread-messages.service.ts @@ -20,8 +20,9 @@ export class UnreadMessagesService { constructor() {} private unreadChatsCount$ = this.timerService - .interval('fetchUnreadChats', 5000) + .interval('fetchUnreadChats', 60_000) .pipe( + startWith(-1), filter(() => !this.location.path().includes('chat')), filter(() => this.profileService.getProfile()?.id !== null), switchMap(() => { @@ -42,7 +43,7 @@ export class UnreadMessagesService { return NEVER; }), startWith(0), - shareReplay(1), + shareReplay(1) ); public unreadChatsCount: Signal = toSignal(this.unreadChatsCount$, { From 240b366b6d81a771e20429a1a4ccf7a881a9056f Mon Sep 17 00:00:00 2001 From: Dominik Halfkann Date: Thu, 28 Dec 2023 20:00:21 +0100 Subject: [PATCH 3/8] =?UTF-8?q?change=20Navigation:=20remove=20"Profil=20b?= =?UTF-8?q?earbeiten"=20and=20rename=20"=C3=9Cbersicht"=20to=20"Empfehlung?= =?UTF-8?q?en"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../logged-in-navigation/logged-in-navigation.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/shared/ui/layout/navigation/logged-in-navigation/logged-in-navigation.component.ts b/src/app/shared/ui/layout/navigation/logged-in-navigation/logged-in-navigation.component.ts index 7a68f8f7..7d55f7a4 100644 --- a/src/app/shared/ui/layout/navigation/logged-in-navigation/logged-in-navigation.component.ts +++ b/src/app/shared/ui/layout/navigation/logged-in-navigation/logged-in-navigation.component.ts @@ -46,7 +46,7 @@ export class LoggedInNavigationComponent { profilePopupOpen = false; menuItems: MenuItem[] = [ - { name: 'Übersicht', route: '/recommendations' }, + { name: 'Empfehlungen', route: '/recommendations' }, { name: 'Nachrichten', route: '/chat' }, { name: 'Über Uns', route: '/about-us' }, { name: 'Kontakt', route: '/contact' }, @@ -54,7 +54,6 @@ export class LoggedInNavigationComponent { profileMenuItems: MenuItem[] = [ { name: 'Dein Profil', route: '/profile' }, - { name: 'Profil bearbeiten', route: '/profile/edit' }, { name: 'Logout', route: '/logout' }, ]; From a4fd83fc10dffc74686eb7b67eea370667c71d0a Mon Sep 17 00:00:00 2001 From: Dominik Halfkann Date: Thu, 28 Dec 2023 20:03:25 +0100 Subject: [PATCH 4/8] fix fetching recommendations twice --- .../recommendation/data-access/recommendation.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/recommendation/data-access/recommendation.service.ts b/src/app/recommendation/data-access/recommendation.service.ts index bb030385..a75d97c8 100644 --- a/src/app/recommendation/data-access/recommendation.service.ts +++ b/src/app/recommendation/data-access/recommendation.service.ts @@ -30,10 +30,10 @@ export class RecommendationService { }; } return response; - }), - shareReplay(1) + }) ) - ) + ), + shareReplay(1) ); getRecommendations$(): Observable> { From 3b39b1fbd824b42aabacf366a84c597e66e0b3df Mon Sep 17 00:00:00 2001 From: Dominik Halfkann Date: Thu, 28 Dec 2023 22:09:38 +0100 Subject: [PATCH 5/8] improve chat (pls test there seem to be some bugs...) --- package-lock.json | 48 +++++++++---------- package.json | 6 +-- .../chat/data-access/chat-state.adapter.ts | 30 ++++++------ .../chat/data-access/chat-state.service.ts | 18 ++++--- 4 files changed, 54 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index 96520843..34b7dbf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,9 +22,9 @@ "@angular/router": "^16.1.4", "@ngneat/until-destroy": "^9.2.2", "@ngrx/component-store": "^16.1.0", - "@state-adapt/angular": "^1.2.1", - "@state-adapt/core": "^1.2.1", - "@state-adapt/rxjs": "^1.2.1", + "@state-adapt/angular": "^2.0.5", + "@state-adapt/core": "^2.0.5", + "@state-adapt/rxjs": "^2.0.5", "bootstrap-icons": "^1.11.2", "date-fns": "^2.29.3", "hammerjs": "^2.0.8", @@ -5431,34 +5431,34 @@ } }, "node_modules/@state-adapt/angular": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@state-adapt/angular/-/angular-1.2.1.tgz", - "integrity": "sha512-HWs5aDrKqc1u+hSmu0j79cNEqqKxo92IyI5scSyTDaBb2Krb89tPoI2m1MqQy5SO2YLU3629QfqgB2Il6XgCpQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@state-adapt/angular/-/angular-2.0.5.tgz", + "integrity": "sha512-eq3x8ycgJea8//Fo8E9iQy5AOKK0rItGjrX0cQSUnC4HKv5xEHiLwMNKQ/ZZ9HG6KltAKKPHN+otA+WN5uRwmg==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/core": "^14.0.0 || ^15.0.0 || ^16.0.0", - "@state-adapt/rxjs": "1.2.1" + "@angular/core": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", + "@state-adapt/rxjs": "2.0.5" } }, "node_modules/@state-adapt/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@state-adapt/core/-/core-1.2.1.tgz", - "integrity": "sha512-zCTgpDn5fN40uuJcYp2iq+jk1DKhJxZsP4hj2ZlPXkMu4b4jzqs3dB60yWEtptJV4Id30K2gqLaAPdwtcuwHzw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@state-adapt/core/-/core-2.0.5.tgz", + "integrity": "sha512-6PQwFEjqVtQfiRA23QBHx88JSqb0BW5TetdKXcvnlROmDX+I5tY9uizrA01EaksD8VjMerP/ym2P/HtBqLd33w==", "dependencies": { "tslib": "^2.0.0" } }, "node_modules/@state-adapt/rxjs": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@state-adapt/rxjs/-/rxjs-1.2.1.tgz", - "integrity": "sha512-hy1sDIVqcI8J/W53v7EdUuMfIumb/SG0rGIKdhT6pVdvSr4sk/zQ9/Ly7hZL0iWaxCTSXtVbLliawFtWphDpOA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@state-adapt/rxjs/-/rxjs-2.0.5.tgz", + "integrity": "sha512-A2MfBOGckUSIhISF63+bjRMh/nveHjw9OPxh1LCKjtyHMJfeFIxTDVxJMfwC5ls/D94wD/Axa3JhC3JoVye5Wg==", "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { - "@state-adapt/core": "1.2.1", + "@state-adapt/core": "2.0.5", "rxjs": "^6.6.6 || ^7.4.0" } }, @@ -25102,25 +25102,25 @@ } }, "@state-adapt/angular": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@state-adapt/angular/-/angular-1.2.1.tgz", - "integrity": "sha512-HWs5aDrKqc1u+hSmu0j79cNEqqKxo92IyI5scSyTDaBb2Krb89tPoI2m1MqQy5SO2YLU3629QfqgB2Il6XgCpQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@state-adapt/angular/-/angular-2.0.5.tgz", + "integrity": "sha512-eq3x8ycgJea8//Fo8E9iQy5AOKK0rItGjrX0cQSUnC4HKv5xEHiLwMNKQ/ZZ9HG6KltAKKPHN+otA+WN5uRwmg==", "requires": { "tslib": "^2.3.0" } }, "@state-adapt/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@state-adapt/core/-/core-1.2.1.tgz", - "integrity": "sha512-zCTgpDn5fN40uuJcYp2iq+jk1DKhJxZsP4hj2ZlPXkMu4b4jzqs3dB60yWEtptJV4Id30K2gqLaAPdwtcuwHzw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@state-adapt/core/-/core-2.0.5.tgz", + "integrity": "sha512-6PQwFEjqVtQfiRA23QBHx88JSqb0BW5TetdKXcvnlROmDX+I5tY9uizrA01EaksD8VjMerP/ym2P/HtBqLd33w==", "requires": { "tslib": "^2.0.0" } }, "@state-adapt/rxjs": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@state-adapt/rxjs/-/rxjs-1.2.1.tgz", - "integrity": "sha512-hy1sDIVqcI8J/W53v7EdUuMfIumb/SG0rGIKdhT6pVdvSr4sk/zQ9/Ly7hZL0iWaxCTSXtVbLliawFtWphDpOA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@state-adapt/rxjs/-/rxjs-2.0.5.tgz", + "integrity": "sha512-A2MfBOGckUSIhISF63+bjRMh/nveHjw9OPxh1LCKjtyHMJfeFIxTDVxJMfwC5ls/D94wD/Axa3JhC3JoVye5Wg==", "requires": { "tslib": "^2.0.0" } diff --git a/package.json b/package.json index dee47aff..dec1d722 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,9 @@ "@angular/router": "^16.1.4", "@ngneat/until-destroy": "^9.2.2", "@ngrx/component-store": "^16.1.0", - "@state-adapt/angular": "^1.2.1", - "@state-adapt/core": "^1.2.1", - "@state-adapt/rxjs": "^1.2.1", + "@state-adapt/angular": "^2.0.5", + "@state-adapt/core": "^2.0.5", + "@state-adapt/rxjs": "^2.0.5", "bootstrap-icons": "^1.11.2", "date-fns": "^2.29.3", "hammerjs": "^2.0.8", diff --git a/src/app/chat/data-access/chat-state.adapter.ts b/src/app/chat/data-access/chat-state.adapter.ts index 2addf10b..edcb2f90 100644 --- a/src/app/chat/data-access/chat-state.adapter.ts +++ b/src/app/chat/data-access/chat-state.adapter.ts @@ -128,8 +128,23 @@ export const chatStateAdapter = createAdapter()({ ...state, }), - setMessagesAsRead: (state, _chatId: string) => ({ + setMessagesAsRead: (state, { chatId, profileId }) => ({ ...state, + chats: state.chats.map((chat) => { + if (chat.id !== chatId) return chat; + return { + ...chat, + lastMessage: chat.lastMessage + ? { + ...(chat.lastMessage || {}), + readByParticipants: [ + ...(chat.lastMessage?.readByParticipants || []), + profileId, + ], + } + : null, + }; + }), }), selectors: { @@ -186,18 +201,5 @@ function updateChat( } return oldChat; - // Object.keys(newChat).forEach((key: string) => { - // const keyOfChat = key as keyof SingleChatState; - // if (keyOfChat !== 'messages' && newChat[keyOfChat] !== oldChat[keyOfChat]) { - // console.log( - // 'updating', - // keyOfChat, - // newChat[keyOfChat], - // oldChat[keyOfChat] - // ); - // const newValue = newChat[keyOfChat]; - // oldChat[keyOfChat] = newValue as any; - // } - // }); return oldChat; } diff --git a/src/app/chat/data-access/chat-state.service.ts b/src/app/chat/data-access/chat-state.service.ts index e082752d..ed359a03 100644 --- a/src/app/chat/data-access/chat-state.service.ts +++ b/src/app/chat/data-access/chat-state.service.ts @@ -76,16 +76,17 @@ export class ChatStateService { newMessageSent: false, }; - private chatStore = adapt( - [this.storePath, this.initialState, chatStateAdapter], - (store) => { + private chatStore = adapt(this.initialState, { + path: this.storePath, + adapter: chatStateAdapter, + sources: (store) => { const timerService = inject(TimerService); const chatHttpService = inject(ChatHttpService); const fetchChatsSources = getRequestSources( '[Chat] fetchChats', merge( - timerService.interval('chatFetchTrigger', 10000), + timerService.interval('chatFetchTrigger', 20000), store.chatCreated$.pipe(filter((hasCreated) => !!hasCreated)) ).pipe( startWith(-1), @@ -180,7 +181,10 @@ export class ChatStateService { for (const message of unreadMessages) { chatHttpService.setMessageAsRead(message.id).subscribe(); } - return of(chatId); + return of({ + chatId: chatId, + profileId: this.profileService.getProfile()!.id!, + }); }), toSource('[Chat] setMessagesAsRead') ); @@ -202,8 +206,8 @@ export class ChatStateService { setMessagesAsRead: setMessagesAsReadSource, reset: this.userLoggedOut$, }; - } - ); + }, + }); // Selectors // public signals for components to consume From c22d3a6feab947ddb8f06da4a572acb13d34e81e Mon Sep 17 00:00:00 2001 From: Dominik Halfkann Date: Sun, 7 Jan 2024 13:58:22 +0100 Subject: [PATCH 6/8] improve chat, fix read flag handling --- .../chat/data-access/chat-state.adapter.ts | 68 ++++++++++++------- .../chat/data-access/chat-state.service.ts | 47 +++++++------ src/app/chat/data-access/chat.types.ts | 12 +++- .../chat-messages/chat-messages.component.ts | 6 +- .../chat-single-message.component.ts | 7 +- .../data-access/chat/chat-http.service.ts | 5 +- .../dancier-backend-mocked.service.ts | 6 +- src/main.ts | 4 ++ 8 files changed, 98 insertions(+), 57 deletions(-) diff --git a/src/app/chat/data-access/chat-state.adapter.ts b/src/app/chat/data-access/chat-state.adapter.ts index edcb2f90..7be1c667 100644 --- a/src/app/chat/data-access/chat-state.adapter.ts +++ b/src/app/chat/data-access/chat-state.adapter.ts @@ -74,27 +74,30 @@ export const chatStateAdapter = createAdapter()({ activeChatId: chatId, }), - chatMessagesFetched: (state, { messages, chatId }: MessagesWithChatId) => ({ - ...state, - newMessageSent: false, - chats: state.chats.map((chat) => { - // only add new messages to active chat - if (chat.id !== chatId) return chat; - return { - ...chat, - messages: [ - ...chat.messages.map((message) => { - const updatedMessage = messages.find((m) => m.id === message.id); - if (!updatedMessage) return message; - return updateMessage(message, updatedMessage); - }), - ...messages.filter( - (message) => !chat.messages.find((m) => m.id === message.id) - ), - ], - }; - }), - }), + chatMessagesFetched: (state, { messages, chatId }: MessagesWithChatId) => { + messages.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); + return { + ...state, + newMessageSent: false, + chats: state.chats.map((chat) => { + // only add new messages to active chat + if (chat.id !== chatId) return chat; + return { + ...chat, + messages: [ + ...chat.messages.map((message) => { + const updatedMessage = messages.find((m) => m.id === message.id); + if (!updatedMessage) return message; + return updateMessage(message, updatedMessage); + }), + ...messages.filter( + (message) => !chat.messages.find((m) => m.id === message.id) + ), + ], + }; + }), + }; + }, openChatWith: (state, dancerId: string) => ({ ...state, @@ -136,9 +139,9 @@ export const chatStateAdapter = createAdapter()({ ...chat, lastMessage: chat.lastMessage ? { - ...(chat.lastMessage || {}), + ...chat.lastMessage, readByParticipants: [ - ...(chat.lastMessage?.readByParticipants || []), + ...(chat.lastMessage.readByParticipants || []), profileId, ], } @@ -147,6 +150,11 @@ export const chatStateAdapter = createAdapter()({ }), }), + profileIdChanged: (state, profileId: string | undefined) => ({ + ...state, + ownProfileId: profileId, + }), + selectors: { chats: (state) => state.chats, chatsFetchState: (state) => state.chatsFetchState, @@ -171,6 +179,16 @@ export const chatStateAdapter = createAdapter()({ return activeChat?.participants ?? []; }, newMessageSent: (state) => state.newMessageSent, + activeChatUnreadMessages: (state) => { + const activeChat = state.chats.find( + (chat) => chat.id === state.activeChatId + ); + if (!activeChat) return []; + return activeChat.messages.filter( + (message) => + message.readByParticipants && message.readByParticipants.length < 2 + ); + }, }, }); @@ -195,11 +213,9 @@ function updateChat( ): SingleChatState { // for each property in newChat, update oldChat, only if they are different if ( - JSON.stringify(oldChat.lastMessage) === JSON.stringify(newChat.lastMessage) + JSON.stringify(oldChat.lastMessage) !== JSON.stringify(newChat.lastMessage) ) { oldChat.lastMessage = newChat.lastMessage; } return oldChat; - - return oldChat; } diff --git a/src/app/chat/data-access/chat-state.service.ts b/src/app/chat/data-access/chat-state.service.ts index ed359a03..5297a3c0 100644 --- a/src/app/chat/data-access/chat-state.service.ts +++ b/src/app/chat/data-access/chat-state.service.ts @@ -5,12 +5,14 @@ import { getRequestSources, Source, toSource } from '@state-adapt/rxjs'; import { ChatMessage, ChatParticipant } from './chat.types'; import { toSignal } from '@angular/core/rxjs-interop'; import { - distinct, + debounceTime, + distinctUntilChanged, filter, map, merge, of, switchMap, + throttleTime, withLatestFrom, } from 'rxjs'; import { HttpErrorResponse } from '@angular/common/http'; @@ -36,6 +38,7 @@ export type ChatAdaptState = { openChatWithParticipantId: string | null; chatCreated: boolean; newMessageSent: boolean; + ownProfileId: string | undefined; }; @Injectable() @@ -64,6 +67,11 @@ export class ChatStateService { sendMessage$ = new Source('[Chat] sendMessage'); + profileIdChanged$ = this.profileService.profile$.pipe( + map((profile) => profile?.id), + toSource('[Chat] profileChanged') + ); + // Adapters // private readonly initialState: ChatAdaptState = { @@ -74,6 +82,7 @@ export class ChatStateService { openChatWithParticipantId: null, chatCreated: false, newMessageSent: false, + ownProfileId: undefined, }; private chatStore = adapt(this.initialState, { @@ -98,6 +107,7 @@ export class ChatStateService { '[Chat] fetchParticipantDetails', store.participantsWithNoDetails$.pipe( filter((participants) => participants.length > 0), + debounceTime(250), map((participants: ChatParticipant[]) => participants.map((p) => p.id) ), @@ -111,7 +121,8 @@ export class ChatStateService { '[Chat] fetchMessages', merge( timerService.interval('chatMessagesFetchTrigger', 5000), - store.activeChatId$.pipe(distinct()) + store.activeChatId$.pipe(distinctUntilChanged()), + store.newMessageSent$.pipe(filter((hasSent) => !!hasSent)) ).pipe( switchMap(() => store.activeChatId$), filter((chatId) => chatId !== null), @@ -158,26 +169,17 @@ export class ChatStateService { ) ); - const setMessagesAsReadSource = store.activeChatId$.pipe( - filter((chatId) => chatId !== null), - filter(() => this.profileService.getProfile()?.id !== undefined), - distinct(), - switchMap((chatId) => - store.chats$.pipe( - map((chats) => chats.find((chat) => chat.id === chatId)), - filter((chat) => chat !== undefined), - map((chat) => ({ - chatId: chat!.id, - unreadMessages: chat!.messages.filter( - (message) => - !message.readByParticipants?.includes( - this.profileService.getProfile()!.id! - ) - ), - })) - ) - ), - switchMap(({ chatId, unreadMessages }) => { + const distinctUnreadMessages = store.activeChatUnreadMessages$.pipe( + distinctUntilChanged( + (curr, prev) => JSON.stringify(curr) === JSON.stringify(prev) + ) + ); + + const setMessagesAsReadSource = distinctUnreadMessages.pipe( + filter((messages) => messages.length > 0), + withLatestFrom(store.activeChatId$), + throttleTime(250), + switchMap(([unreadMessages, chatId]) => { for (const message of unreadMessages) { chatHttpService.setMessageAsRead(message.id).subscribe(); } @@ -204,6 +206,7 @@ export class ChatStateService { messageSent: sendMessageSource.success$, messageSentError: sendMessageSource.error$, setMessagesAsRead: setMessagesAsReadSource, + profileIdChanged: this.profileIdChanged$, reset: this.userLoggedOut$, }; }, diff --git a/src/app/chat/data-access/chat.types.ts b/src/app/chat/data-access/chat.types.ts index 7092dfec..059dd737 100644 --- a/src/app/chat/data-access/chat.types.ts +++ b/src/app/chat/data-access/chat.types.ts @@ -6,7 +6,7 @@ export type ChatDto = { lastMessage: ChatMessage | null; }; -export type ChatMessage = { +export type ChatMessageDto = { text: string; authorId: DancerId; id: string; @@ -14,6 +14,14 @@ export type ChatMessage = { createdAt: string; }; +export type ChatMessage = { + text: string; + authorId: DancerId; + id: string; + readByParticipants: DancerId[] | null; + createdAt: Date; +}; + export type ChatList = { chats: ChatDto[]; }; @@ -44,7 +52,7 @@ export type ChatsAndDancers = { dancerMap: DancerMapDto; }; -export type MessageResponse = ChatMessage[]; +export type MessageResponse = ChatMessageDto[]; export type MessagesWithChatId = { messages: ChatMessage[]; diff --git a/src/app/chat/ui/chat-messages/chat-messages.component.ts b/src/app/chat/ui/chat-messages/chat-messages.component.ts index 0e6473bd..ca894925 100644 --- a/src/app/chat/ui/chat-messages/chat-messages.component.ts +++ b/src/app/chat/ui/chat-messages/chat-messages.component.ts @@ -28,7 +28,11 @@ import { map } from 'rxjs'; class="max-w-[80%]" [message]="message" [isOwnMessage]="message.authorId === ownUserId()" - [isRead]="message.readByParticipants?.includes(ownUserId()!) || false" + [isRead]=" + message.readByParticipants + ? message.readByParticipants.length > 1 + : false + " [ngClass]="{ 'self-start': message.authorId !== ownUserId(), 'self-end': message.authorId === ownUserId() diff --git a/src/app/chat/ui/chat-messages/chat-single-message.component.ts b/src/app/chat/ui/chat-messages/chat-single-message.component.ts index 811a77b9..92dc790d 100644 --- a/src/app/chat/ui/chat-messages/chat-single-message.component.ts +++ b/src/app/chat/ui/chat-messages/chat-single-message.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChatMessage } from '../../data-access/chat.types'; -import { NgClass, NgIf } from '@angular/common'; +import { DatePipe, NgClass, NgIf } from '@angular/common'; @Component({ selector: 'app-chat-single-message', @@ -33,10 +33,13 @@ import { NgClass, NgIf } from '@angular/common'; +
+ {{ message.createdAt | date : 'short' }} +
`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [NgIf, NgClass], + imports: [NgIf, NgClass, DatePipe], }) export class ChatSingleMessageComponent { @Input({ required: true }) diff --git a/src/app/shared/data-access/chat/chat-http.service.ts b/src/app/shared/data-access/chat/chat-http.service.ts index 3c6dc163..fce2b064 100644 --- a/src/app/shared/data-access/chat/chat-http.service.ts +++ b/src/app/shared/data-access/chat/chat-http.service.ts @@ -153,7 +153,10 @@ export class ChatHttpService { .pipe( map((messageResponse) => ({ chatId, - messages: messageResponse, + messages: messageResponse.map((message) => ({ + ...message, + createdAt: new Date(message.createdAt), + })), })) ); } diff --git a/src/app/shared/data-access/dancier-backend-mocked.service.ts b/src/app/shared/data-access/dancier-backend-mocked.service.ts index ef74f26f..b6be87f7 100644 --- a/src/app/shared/data-access/dancier-backend-mocked.service.ts +++ b/src/app/shared/data-access/dancier-backend-mocked.service.ts @@ -57,14 +57,14 @@ const chatMessages: MessagesWithChatId = { id: 'messageId1', authorId: 'dancerId1', text: 'Message 1', - createdAt: '2021-01-01T00:00:00.000Z', + createdAt: new Date('2021-01-01T00:00:00.000Z'), readByParticipants: [], }, { id: 'messageId2', authorId: 'dancerId2', text: 'Message 2', - createdAt: '2021-01-01T00:00:00.000Z', + createdAt: new Date('2021-01-01T00:00:00.000Z'), readByParticipants: [], }, ], @@ -115,7 +115,7 @@ export class DancierBackendMockedService { id: 'messageId' + chatMessages.messages.length + 1, authorId: 'dancerId1', text: message, - createdAt: '2021-01-01T00:00:00.000Z', + createdAt: new Date('2021-01-01T00:00:00.000Z'), readByParticipants: [], }); return chatMessages; diff --git a/src/main.ts b/src/main.ts index b930e5bc..b8d427a9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,11 +6,15 @@ import { environment } from './environments/environment'; import { AppComponent } from './app/app.component'; import { bootstrapApplication } from '@angular/platform-browser'; import { APP_CONFIG } from './app/app.config'; +import { registerLocaleData } from '@angular/common'; +import * as de from '@angular/common/locales/de'; if (environment.production) { enableProdMode(); } +registerLocaleData(de.default, 'de-DE'); + bootstrapApplication(AppComponent, APP_CONFIG).catch((err) => console.error(err) ); From 22d2d062e474cc8fc3f488419b91c8403b32bac2 Mon Sep 17 00:00:00 2001 From: Dominik Halfkann Date: Sun, 7 Jan 2024 15:26:43 +0100 Subject: [PATCH 7/8] fix tests --- src/app/recommendation/recommendation.spec.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/app/recommendation/recommendation.spec.ts b/src/app/recommendation/recommendation.spec.ts index d85770be..4cbe891b 100644 --- a/src/app/recommendation/recommendation.spec.ts +++ b/src/app/recommendation/recommendation.spec.ts @@ -11,6 +11,9 @@ import { RecommendationService } from './data-access/recommendation.service'; import { RecommendationHttpService } from './data-access/recommendation-http.service'; import recommendationsJson from './util/fixtures/recommendations.json'; +import { MockProvider } from 'ng-mocks'; +import { TimerService } from '@shared/util/time/timer.service'; +import { of } from 'rxjs'; describe('Recommendation Feature', () => { describe('when the user is on the recommendation page', () => { @@ -18,7 +21,13 @@ describe('Recommendation Feature', () => { const createComponent = createRoutingFactory({ component: RecommendationsComponent, imports: [RecommendedDancerComponent, HttpClientTestingModule], - providers: [RecommendationService, RecommendationHttpService], + providers: [ + RecommendationService, + RecommendationHttpService, + MockProvider(TimerService, { + interval: () => of(0), + }), + ], stubsEnabled: false, routes: [ { @@ -47,7 +56,7 @@ describe('Recommendation Feature', () => { } }); - it('shows a list of 4 recommendations', async () => { + it('shows a list of 4 recommendations', () => { expect(spectator.queryAll('app-recommended-dancer')).toHaveLength(4); }); }); From c9e2827a7794fda37460c13ecbc70d3da893439e Mon Sep 17 00:00:00 2001 From: Dominik Halfkann Date: Sun, 7 Jan 2024 15:53:21 +0100 Subject: [PATCH 8/8] fix jest and cypress global type definitions clashing in IntelliJ --- src/tsconfig.spec.json | 21 +++++++++++++++++++++ tsconfig.spec.json | 24 +++++++++--------------- 2 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 src/tsconfig.spec.json diff --git a/src/tsconfig.spec.json b/src/tsconfig.spec.json new file mode 100644 index 00000000..946bd716 --- /dev/null +++ b/src/tsconfig.spec.json @@ -0,0 +1,21 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "module": "CommonJs", + "skipLibCheck": true, + "emitDecoratorMetadata": true + }, + "files": [ + "polyfills.ts" + ], + "include": [ + "**/*.spec.ts", + ], + "exclude": [ + // This is to prevent global types clashing between jest and cypress + // See: https://github.com/cypress-io/cypress/issues/22059#issuecomment-1148921141 + "../cypress.config.ts", + ] +} diff --git a/tsconfig.spec.json b/tsconfig.spec.json index da383646..55dcb9ab 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -1,17 +1,11 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ +/* Having the real tsconfig.spec.json under src is a workaround, as in IntelliJ, + * the *.spec.ts files would otherwise be compiled with tsconfig.app.json, + * which would lead to a clash of global types for tests that both cypress and jest define. + * + * See: + * - https://stackoverflow.com/questions/58999086/cypress-causing-type-errors-in-jest-assertions/72663546#72663546 + * - https://youtrack.jetbrains.com/issue/WEB-43373/IntelliJ-not-correctly-interpreting-standard-ng-cli-tsconfig-setup + */ { - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./out-tsc/spec", - "module": "CommonJs", - "types": ["jest"], - "skipLibCheck": true, - "emitDecoratorMetadata": true - }, - "files": ["src/polyfills.ts"], - "include": [ - "src/**/*.spec.ts", - "src/**/*.d.ts", - "../node_modules/@types/jest" - ] + "extends": "./src/tsconfig.spec.json" }