Skip to content

Commit

Permalink
display an amount of chats with unread messages (#2317)
Browse files Browse the repository at this point in the history
* added method of unreadMessages in chat

* Removed question mark in translation

* Add applications count example endpoint, state, badges for cabinet

* Add provider.service tests

* Update chat.state.ts

* Update uk.json

* Changed endpoint, refactored application service docs, moved method to application service, add tests

* Made pendingApplications as search response, add role check

* Add null-safety for template

* Add test

* Add support icon & fixed notification badge style

* Add unreadNotificationsCount support

* Removed notifications-list amount test assertions since it's managed by notifications.component

* Add accepted for selection notification declination

* Fixed fetching of pending application after application update and badge empty bug

* Fixed changing notifications amount by tab navigation

* Test enhancement

* Resolved code review comments

* Removed unused imports in personal-cabinet.component

---------

Co-authored-by: doliinyk <denysoliinyk@outlook.com>
Co-authored-by: Denys Oliinyk <68949838+doliinyk@users.noreply.github.com>
  • Loading branch information
3 people authored Jul 9, 2024
1 parent 6315311 commit f4112a7
Show file tree
Hide file tree
Showing 22 changed files with 502 additions and 100 deletions.
5 changes: 3 additions & 2 deletions src/app/header/header.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
</a>

<a class="info-link" [routerLink]="'./info/support'">
<mat-icon>support</mat-icon>
{{ 'ENUM.NAV_BAR_NAME.SUPPORT' | translate | uppercase }}
</a>

Expand Down Expand Up @@ -64,9 +65,9 @@ <h2 class="header-descr">{{ headerSubtitle }}</h2>
<app-notifications class="menu" *ngIf="(featuresList$ | async)?.release2"></app-notifications>

<button mat-flat-button class="menu" [matMenuTriggerFor]="log">
<mat-icon>account_circle</mat-icon>
<mat-icon class="account-circle">account_circle</mat-icon>
{{ userShortName }}
<mat-icon>arrow_drop_down</mat-icon>
<mat-icon class="arrow-drop-down">arrow_drop_down</mat-icon>
</button>
</ng-container>

Expand Down
53 changes: 21 additions & 32 deletions src/app/header/header.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,21 @@
}

.info-link {
display: flex;
background: none;
color: white;
align-items: center;
justify-content: flex-end;
font-size: 0.7rem;
font-family: 'Innerspace';
font-weight: bold;
white-space: nowrap;
line-height: 24px;
cursor: pointer;

.mat-icon {
margin-right: 5px;
}
}

.login {
Expand Down Expand Up @@ -107,52 +113,35 @@
font-size: 0.7rem !important;
}

.lang mat-icon {
position: relative;
top: -1px;
}

.account-button {
font-weight: bold !important;
display: block;

.icon-account {
color: white;
.menu .mat-icon {
&.account-circle {
margin-right: 5px;
padding-left: 5px;
}

.account-circle {
width: 33px;
padding: 0 5px;
min-width: 0;
height: 33px;
background-color: transparent;
box-shadow: none;
top: -2px;
left: 5px;

::ng-deep.mat-badge-content {
font-size: xx-small;
height: 15px;
width: 15px;
line-height: 15px;
top: 0;
left: 0 !important;
position: absolute;
background-color: #f5bc4a;
}
&.arrow-drop-down {
margin-left: 2.5px;
}
}

.lang .mat-icon {
margin-right: 5px;
top: -1px;
}

::ng-deep .lang-option {
width: auto !important;
}

::ng-deep .lang-option .mat-select-value {
color: white !important;
margin-left: 12px !important;
overflow: visible;
padding-right: 0px !important;
font-family: 'Innerspace';

@media (max-width: 750px) {
margin-left: 12px !important;
}
}

::ng-deep .lang-option .mat-select-arrow {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ <h3 class="notifications-list__title">{{ 'NOTIFICATIONS' | translate | uppercase
class="body-notification__content body-notification__group info"
(click)="onNavigate(notification)">
<span class="info__description info__link">
{{ notification.amount | translateCases: defineDeclination(notification) }}
{{ notification.amount || null | translateCases: defineDeclination(notification) }}
</span>
<mat-icon class="icon icon__link">navigate_next</mat-icon>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import { NgxsModule, State, Store } from '@ngxs/store';
import { NotificationDeclination } from 'shared/enum/enumUA/declinations/notification-declination';
import { NotificationAction, NotificationType } from 'shared/enum/notifications';
import { PersonalCabinetLinks } from 'shared/enum/personal-cabinet-links';
import { Role } from 'shared/enum/role';
import { Notification, NotificationGrouped } from 'shared/models/notification.model';
import { MaterialModule } from 'shared/modules/material.module';
import { TranslateCasesPipe } from 'shared/pipes/translate-cases.pipe';
import { ChatStateModel } from 'shared/store/chat.state';
import { ReadAllUsersNotifications, ReadUsersNotificationById, ReadUsersNotificationsByType } from 'shared/store/notification.actions';
import { NotificationStateModel } from 'shared/store/notification.state';
import { RegistrationStateModel } from 'shared/store/registration.state';
import { NotificationsListComponent } from './notifications-list.component';

describe('NotificationsListComponent', () => {
Expand All @@ -26,7 +28,7 @@ describe('NotificationsListComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
NgxsModule.forRoot([MockNotificationState, MockChatState]),
NgxsModule.forRoot([MockNotificationState, MockChatState, MockRegistrationState]),
MaterialModule,
RouterModule,
TranslateModule.forRoot(),
Expand Down Expand Up @@ -275,7 +277,6 @@ describe('NotificationsListComponent', () => {
describe('notification group', () => {
let notification: Notification;
let initialNotificationsGroupedByTypeLength: number;
let initialNotificationAmount: number;

beforeEach(() => {
notification = {
Expand All @@ -286,7 +287,6 @@ describe('NotificationsListComponent', () => {
data: { Status: 'Pending' }
};
initialNotificationsGroupedByTypeLength = component.notificationsGroupedByType.length;
initialNotificationAmount = component.notificationAmount.amount;
});

it('should add amount to notifications grouped when received with existing additional data', () => {
Expand All @@ -305,7 +305,6 @@ describe('NotificationsListComponent', () => {
expect(component.notificationsGroupedByType[0].groupedByAdditionalData[0].amount).toEqual(
initialNotificationsGroupedByDataAmount + 1
);
expect(component.notificationAmount.amount).toEqual(initialNotificationAmount + 1);
});

it('should add a new notification group to notifications grouped when received with another type', () => {
Expand All @@ -321,7 +320,6 @@ describe('NotificationsListComponent', () => {
});

expect(component.notificationsGroupedByType.length).toEqual(initialNotificationsGroupedByTypeLength + 1);
expect(component.notificationAmount.amount).toEqual(initialNotificationAmount + 1);
});

it('should add a new notification group to notifications grouped when received with another additional data', () => {
Expand All @@ -339,7 +337,6 @@ describe('NotificationsListComponent', () => {

expect(component.notificationsGroupedByType.length).toEqual(initialNotificationsGroupedByTypeLength);
expect(component.notificationsGroupedByType[0].groupedByAdditionalData.length).toEqual(initialNotificationsGroupedByDataLength + 1);
expect(component.notificationAmount.amount).toEqual(initialNotificationAmount + 1);
});
});

Expand All @@ -352,7 +349,6 @@ describe('NotificationsListComponent', () => {
data: {}
};
const initialNotificationsLength = component.notifications.length;
const initialNotificationAmount = component.notificationAmount.amount;

component.ngOnChanges({
receivedNotification: {
Expand All @@ -364,7 +360,6 @@ describe('NotificationsListComponent', () => {
});

expect(component.notifications.length).toEqual(initialNotificationsLength + 1);
expect(component.notificationAmount.amount).toEqual(initialNotificationAmount + 1);
});
});
});
Expand Down Expand Up @@ -394,3 +389,13 @@ class MockNotificationState {}
})
@Injectable()
class MockChatState {}

@State<RegistrationStateModel>({
name: 'registration',
defaults: {
role: Role.provider,
provider: { id: 'providerId' }
} as RegistrationStateModel
})
@Injectable()
class MockRegistrationState {}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
ReadUsersNotificationsByType
} from 'shared/store/notification.actions';
import { NotificationState } from 'shared/store/notification.state';
import { GetPendingApplicationsByProviderId } from 'shared/store/provider.actions';
import { RegistrationState } from 'shared/store/registration.state';
import { Util } from 'shared/utils/utils';

Expand Down Expand Up @@ -179,11 +180,12 @@ export class NotificationsListComponent implements OnInit, OnChanges, OnDestroy
}

private addReceivedNotification(receivedNotification: Notification): void {
this.notificationAmount.amount++;

if (receivedNotification.type !== NotificationType.Application && receivedNotification.type !== NotificationType.Chat) {
this.notifications.unshift(receivedNotification);
return;
} else if (receivedNotification.type === NotificationType.Application && this.role === Role.provider) {
const providerId = this.store.selectSnapshot(RegistrationState.provider).id;
this.store.dispatch(new GetPendingApplicationsByProviderId(providerId));
}

const newNotificationGroupReceived = this.notificationsGroupedByType.every((notificationGroupedByType) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
.mat-icon-button {
margin-right: 10px;
}

:host::ng-deep .mat-icon {
position: relative;
overflow: visible;
font-size: 30px;
top: -1px;

@media (min-width: 750px) {
height: auto;
width: auto;
}

.mat-badge-content {
right: -20px !important;
top: -5px;
background-color: #f5bc4a;
width: 25px;
height: 15px;
border-radius: 13px;
line-height: 15px;
z-index: 10;
}
}

.icon-mobile {
top: 11px;
top: 8px;
margin-left: 73px;
}

Expand Down
40 changes: 32 additions & 8 deletions src/app/shared/components/notifications/notifications.component.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { AfterViewChecked, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable, Subject, combineLatest, filter, takeUntil } from 'rxjs';
import { Actions, Select, Store, ofActionSuccessful } from '@ngxs/store';
import { BehaviorSubject, EMPTY, Observable, Subject, combineLatest, filter, map, switchMap, takeUntil } from 'rxjs';

import { NOTIFICATION_HUB_URL } from 'shared/constants/hubs-url';
import { NotificationType } from 'shared/enum/notifications';
import { Notification, NotificationAmount } from 'shared/models/notification.model';
import { SignalRService } from 'shared/services/signalR/signal-r.service';
import { AppState } from 'shared/store/app.state';
import { GetUnreadMessagesCount } from 'shared/store/chat.actions';
import { ChatState } from 'shared/store/chat.state';
import { GetAmountOfNewUsersNotifications } from 'shared/store/notification.actions';
import { GetAmountOfNewUsersNotifications, ReadUsersNotificationsByType } from 'shared/store/notification.actions';
import { NotificationState } from 'shared/store/notification.state';
import { RegistrationState } from 'shared/store/registration.state';
import { isRoleAdmin } from 'shared/utils/admin.utils';
Expand All @@ -30,12 +31,14 @@ export class NotificationsComponent implements OnInit, AfterViewChecked, OnDestr
public receivedNotification: Notification;
private hubConnection: signalR.HubConnection;

private unreadNotificationsCount$ = new BehaviorSubject<number>(null);
private destroy$: Subject<boolean> = new Subject<boolean>();

constructor(
private store: Store,
private cdr: ChangeDetectorRef,
private signalRService: SignalRService
private signalRService: SignalRService,
private actions$: Actions
) {}

public ngOnInit(): void {
Expand All @@ -48,7 +51,8 @@ export class NotificationsComponent implements OnInit, AfterViewChecked, OnDestr
}
this.hubConnection.on('ReceiveNotification', (receivedNotificationString: string) => {
// TODO: solve the problem with keys with capital letters
const parsedNotification = JSON.parse(receivedNotificationString);
const { unreadNotificationsCount, newNotificationDto: parsedNotification } = JSON.parse(receivedNotificationString);
this.unreadNotificationsCount$.next(unreadNotificationsCount);
this.receivedNotification = {
id: parsedNotification.Id,
userId: parsedNotification.UserId,
Expand All @@ -61,14 +65,34 @@ export class NotificationsComponent implements OnInit, AfterViewChecked, OnDestr
};
});

this.actions$
.pipe(
ofActionSuccessful(ReadUsersNotificationsByType),
switchMap((payload) =>
payload.notificationType === NotificationType.Application && payload.needGetRequest
? this.store.dispatch(new GetAmountOfNewUsersNotifications())
: EMPTY
),
filter(Boolean),
takeUntil(this.destroy$)
)
.subscribe((notificationAmount) => this.unreadNotificationsCount$.next(notificationAmount.amount));

combineLatest([
this.notificationAmount$.pipe(filter(Boolean)),
this.unreadNotificationsCount$,
this.unreadMessagesCount$.pipe(filter((unreadMessagesCount) => isRoleAdmin(role) || unreadMessagesCount !== null))
])
.pipe(takeUntil(this.destroy$))
.pipe(
map(([notificationAmount, unreadNotificationsCount, unreadMessagesCount]: [NotificationAmount, number, number]) => [
unreadNotificationsCount ?? notificationAmount.amount,
unreadMessagesCount
]),
takeUntil(this.destroy$)
)
.subscribe(
([notificationAmount, unreadMessagesCount]: [NotificationAmount, number]) =>
(this.notificationAmount = { amount: notificationAmount.amount + unreadMessagesCount })
([notificationAmount, unreadMessagesCount]: [number, number]) =>
(this.notificationAmount = { amount: notificationAmount + unreadMessagesCount })
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,30 @@ export namespace NotificationDeclination {
}

export namespace Application {
export type NotificationDeclinationType = typeof Changes | typeof Approved | typeof Pending | typeof Rejected | typeof Left;
export type NotificationDeclinationType =
| typeof Changes
| typeof Approved
| typeof AcceptedForSelection
| typeof Pending
| typeof Rejected
| typeof Left;

export enum Changes {
'ENUM.APPLICATION_CHANGES.CHANGE_IN_APPLICATIONS',
'ENUM.APPLICATION_CHANGES.CHANGES_IN_APPLICATIONS',
'ENUM.APPLICATION_CHANGES.CHANGE_IN_APPLICATIONS_ABLATIVE'
}

export enum AcceptedForSelection {
'ENUM.APPLICATION_ACCEPTED_FOR_SELECTION.APPLICATION_ACCEPTED_FOR_SELECTION',
'ENUM.APPLICATION_ACCEPTED_FOR_SELECTION.APPLICATIONS_ACCEPTED_FOR_SELECTION',
'ENUM.APPLICATION_ACCEPTED_FOR_SELECTION.APPLICATION_ACCEPTED_FOR_SELECTION_ABLATIVE'
}

export enum Approved {
'ENUM.APPLICATION_APPROVED.APPLICATION_ACCEPTED',
'ENUM.APPLICATION_APPROVED.APPLICATIONS_ACCEPTED',
'ENUM.APPLICATION_APPROVED.APPLICATION_ACCEPTED_ABLATIVE'
'ENUM.APPLICATION_APPROVED.APPLICATION_APPROVED',
'ENUM.APPLICATION_APPROVED.APPLICATIONS_APPROVED',
'ENUM.APPLICATION_APPROVED.APPLICATION_APPROVED_ABLATIVE'
}

export enum Pending {
Expand Down
Loading

0 comments on commit f4112a7

Please sign in to comment.