From 7376c184a454a0e70e95f62808719783fcccc124 Mon Sep 17 00:00:00 2001 From: sofiia koval Date: Fri, 15 Nov 2024 12:37:19 +0200 Subject: [PATCH 1/9] feat: add redirect to invited habit --- .../user-notifications.component.html | 11 +++- .../user-notifications.component.ts | 60 +++++++++++-------- .../notific-content-replace.directive.ts | 11 ++++ .../user/models/notification.model.ts | 3 +- 4 files changed, 57 insertions(+), 28 deletions(-) diff --git a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.html b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.html index 9b720e4567..f6b0f27c72 100644 --- a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.html +++ b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.html @@ -109,11 +109,16 @@

{{ 'homepage.notifications.title' | translate }}

(click)="navigate($event)" (keydown)="navigate($event)" >

-
- -
diff --git a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts index 38e5c3a6e1..279509d07c 100644 --- a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts +++ b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts @@ -34,6 +34,7 @@ export class UserNotificationsComponent implements OnInit, OnDestroy { projects = projects; notificationTypes = NotificationCriteria; notificationFriendRequest = NotificationCriteria.FRIEND_REQUEST_RECEIVED; + notificationHabitInvitation = NotificationCriteria.HABIT_INVITATION; notifications: NotificationModel[] = []; currentPage = 0; @@ -114,7 +115,7 @@ export class UserNotificationsComponent implements OnInit, OnDestroy { private getAllSelectedFilters(approach: string): NotificationFilter[] { const filterArr = approach === this.filterCriteria.TYPE ? notificationCriteriaOptions : projects; const allOption = filterArr.find((el) => el.name === this.filterAll); - return allOption.isSelected ? [] : [...filterArr.filter((el) => {return el.isSelected === true && el.name !== this.filterAll;})]; + return allOption.isSelected ? [] : [...filterArr.filter((el) => { return el.isSelected === true && el.name !== this.filterAll; })]; } getNotification(page: number): void { @@ -260,47 +261,58 @@ export class UserNotificationsComponent implements OnInit, OnDestroy { } } - acceptRequest(userId: number): void { + acceptRequest(notification: NotificationModel): void { let isAccepted = true; - this.userFriendsService.acceptRequest(userId).subscribe({ - error: () => { - isAccepted = false; - }, - complete: () => { - if (isAccepted) { - this.matSnackBar.openSnackBar('friendInValidRequest'); + + const userId = notification.actionUserId[0]; + if (notification.notificationType === this.notificationFriendRequest) { + this.userFriendsService.acceptRequest(userId).subscribe({ + error: () => { + isAccepted = false; + }, + complete: () => { + if (isAccepted) { + this.matSnackBar.openSnackBar('friendInValidRequest'); + } } - } - }); + }); + } } - declineRequest(userId: number): void { + declineRequest(notification: NotificationModel): void { let isAccepted = true; - this.userFriendsService.declineRequest(userId).subscribe({ - error: () => { - isAccepted = false; - }, - complete: () => { - if (isAccepted) { - this.matSnackBar.openSnackBar('friendInValidRequest'); + const userId = notification.actionUserId[0]; + if (notification.notificationType === this.notificationFriendRequest) { + this.userFriendsService.declineRequest(userId).subscribe({ + error: () => { + isAccepted = false; + }, + complete: () => { + if (isAccepted) { + this.matSnackBar.openSnackBar('friendInValidRequest'); + } } - } - }); + }); + } } navigate(event: Event): void { const target = event.target as HTMLElement; const userId = this.userService.userId; const targetTextContent = target.textContent?.trim() || ''; - const targetUserId = target.getAttribute('data-userid')?.toString(); + const targetUserId = Number(target.getAttribute('data-userId')); const notificationType = target.getAttribute('data-notificationType'); - const targetId = Number(target.getAttribute('data-targetid')); + const targetId = Number(target.getAttribute('data-targetId')); const isClickOrEnter = event instanceof MouseEvent || (event instanceof KeyboardEvent && event.key === 'Enter'); if (!isClickOrEnter) { return; } + if (targetUserId === userId) { + this.router.navigate(['profile', userId]); + return; + } if (targetUserId) { this.router.navigate(['profile', userId, 'users', targetTextContent, targetUserId]); return; @@ -310,7 +322,7 @@ export class UserNotificationsComponent implements OnInit, OnDestroy { const routes = { EVENT: ['events', targetId], ECONEWS: ['news', targetId], - HABIT: ['profile', userId, 'allhabits', 'edithabit', targetId] + HABIT: ['profile', userId, 'allhabits', 'addhabit', targetId] }; for (const type in routes) { diff --git a/src/app/main/component/user/directives/notific-content-replace.directive.ts b/src/app/main/component/user/directives/notific-content-replace.directive.ts index 598a7ed336..2b8d72b0bc 100644 --- a/src/app/main/component/user/directives/notific-content-replace.directive.ts +++ b/src/app/main/component/user/directives/notific-content-replace.directive.ts @@ -45,12 +45,23 @@ export class NotificContentReplaceDirective implements OnChanges { userId: replacements.actionUserId[index] }); } + } else if (contentKey === 'message' && replacements.notificationType) { + const linkAttributes = replacements.targetId + ? { + targetId: replacements.targetId, + notificationType: replacements.notificationType + } + : null; + result = this.buildReplacementString(result, contentKey, replacements[replacementKey], linkAttributes); } else if (replacements.hasOwnProperty(replacementKey)) { const linkAttributes = idToNavigate ? { userId: replacements[idToNavigate] } : null; + result = this.buildReplacementString(result, contentKey, replacements[replacementKey], linkAttributes); + console.log('in 3'); } }); + console.log(result); return result; } diff --git a/src/app/main/component/user/models/notification.model.ts b/src/app/main/component/user/models/notification.model.ts index 0f64000cab..e2d0083e48 100644 --- a/src/app/main/component/user/models/notification.model.ts +++ b/src/app/main/component/user/models/notification.model.ts @@ -52,7 +52,8 @@ export enum NotificationCriteria { EVENT_JOINED = 'EVENT_JOINED', EVENT_COMMENT = ' EVENT_COMMENT', FRIEND_REQUEST_RECEIVED = 'FRIEND_REQUEST_RECEIVED', - FRIEND_REQUEST_ACCEPTED = 'FRIEND_REQUEST_ACCEPTED' + FRIEND_REQUEST_ACCEPTED = 'FRIEND_REQUEST_ACCEPTED', + HABIT_INVITATION = 'HABIT_INVITE' } export const filterCriteriaOptions = [ From fd0297fadd1134e2134fa24843a31cc0d0154b0f Mon Sep 17 00:00:00 2001 From: sofiia koval Date: Tue, 19 Nov 2024 13:53:50 +0200 Subject: [PATCH 2/9] feat: add accent and decline btns to invitation --- .../mat-snack-bar/mat-snack-bar.component.ts | 4 +++ .../user-notifications.component.ts | 33 ++++++++++++++++--- src/app/main/service/habit/habit.service.ts | 8 +++++ src/assets/i18n/en.json | 6 +++- src/assets/i18n/ua.json | 4 +++ 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/app/main/component/errors/mat-snack-bar/mat-snack-bar.component.ts b/src/app/main/component/errors/mat-snack-bar/mat-snack-bar.component.ts index 72ea86d3ce..a6598a455e 100644 --- a/src/app/main/component/errors/mat-snack-bar/mat-snack-bar.component.ts +++ b/src/app/main/component/errors/mat-snack-bar/mat-snack-bar.component.ts @@ -74,6 +74,10 @@ export class MatSnackBarComponent { addFriend: { classname: SnackbarClassName.success, key: 'snack-bar.success.add-friend' }, friendValidation: { classname: SnackbarClassName.error, key: 'snack-bar.error.friend-request' }, friendInValidRequest: { classname: SnackbarClassName.error, key: 'snack-bar.error.friend-already-added' }, + habitAcceptRequest: { classname: SnackbarClassName.success, key: 'snack-bar.error.habit-added-success' }, + habitDeclineRequest: { classname: SnackbarClassName.success, key: 'snack-bar.error.habit-decline-success' }, + habitAcceptInValidRequest: { classname: SnackbarClassName.error, key: 'snack-bar.error.habit-not-added' }, + habitDeclineInValidRequest: { classname: SnackbarClassName.error, key: 'snack-bar.error.habit-already-added' }, cancelRequest: { classname: SnackbarClassName.success, key: 'snack-bar.success.cancel-request' }, jointEventRequest: { classname: SnackbarClassName.success, key: 'snack-bar.success.joint-event-request' }, errorImageTypeSize: { classname: SnackbarClassName.error, key: 'user.photo-upload.error-img-type-and-size' }, diff --git a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts index 279509d07c..72cc43640e 100644 --- a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts +++ b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts @@ -19,6 +19,7 @@ import { Subject } from 'rxjs'; import { debounceTime, take, takeUntil } from 'rxjs/operators'; import { NotificationBody, Notifications } from '@ubs/ubs-admin/models/ubs-user.model'; import { HttpParams } from '@angular/common/http'; +import { HabitService } from '@global-service/habit/habit.service'; @Component({ selector: 'app-user-notifications', @@ -52,7 +53,8 @@ export class UserNotificationsComponent implements OnInit, OnDestroy { private matSnackBar: MatSnackBarComponent, private userFriendsService: UserFriendsService, private router: Router, - private userService: UserService + private userService: UserService, + private readonly habitService: HabitService ) {} ngOnInit() { @@ -262,10 +264,11 @@ export class UserNotificationsComponent implements OnInit, OnDestroy { } acceptRequest(notification: NotificationModel): void { - let isAccepted = true; - - const userId = notification.actionUserId[0]; if (notification.notificationType === this.notificationFriendRequest) { + let isAccepted = true; + + const userId = notification.actionUserId[0]; + this.userFriendsService.acceptRequest(userId).subscribe({ error: () => { isAccepted = false; @@ -276,6 +279,17 @@ export class UserNotificationsComponent implements OnInit, OnDestroy { } } }); + } else if (notification.notificationType === this.notificationHabitInvitation) { + const invitationId = notification.secondMessageId; + + this.habitService.acceptHabitInvitation(invitationId).subscribe({ + next: () => { + this.matSnackBar.openSnackBar('habitAcceptRequest'); + }, + error: () => { + this.matSnackBar.openSnackBar('habitAcceptInValidRequest'); + } + }); } } @@ -293,6 +307,17 @@ export class UserNotificationsComponent implements OnInit, OnDestroy { } } }); + } else if (notification.notificationType === this.notificationHabitInvitation) { + const invitationId = notification.secondMessageId; + + this.habitService.declineHabitInvitation(invitationId).subscribe({ + next: () => { + this.matSnackBar.openSnackBar('habitDeclineRequest'); + }, + error: () => { + this.matSnackBar.openSnackBar('habitDeclineInValidRequest'); + } + }); } } diff --git a/src/app/main/service/habit/habit.service.ts b/src/app/main/service/habit/habit.service.ts index abae933d90..b00686f422 100644 --- a/src/app/main/service/habit/habit.service.ts +++ b/src/app/main/service/habit/habit.service.ts @@ -84,6 +84,14 @@ export class HabitService { return this.http.delete(`${habitLink}/delete/${id}`); } + acceptHabitInvitation(invitationId: number) { + return this.http.patch(`${habitLink}/invite/${invitationId}/accept`, {}); + } + + declineHabitInvitation(invitationId: number) { + return this.http.patch(`${habitLink}/invite/${invitationId}/reject`, {}); + } + private prepareCustomHabitRequest(habit: CustomHabit, lang: string): FormData { const body = { habitTranslations: [ diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 36fd1b4b2c..c7010bfae7 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -511,7 +511,11 @@ "friend-request": "You cannot add this friend", "friend-already-added": "This friend has been already added", "error-create-address": "Oops, An error occurred while creating the address. Please try again.", - "error-edit-address": "Oops, An error occurred while edititng the address. Please try again." + "habit-not-added": "This habit invitation can`t be accepted", + "habit-already-added": "This habit invitation can`t be declined", + "habit-added-success": "Habit invitation was accepted", + "habit-decline-success": "Habit invitation was declined", + "error-edit-address": "Oops, An error occurred while editing the address. Please try again." }, "attention": { "default": "Attention message. The action cannot be performed, because conditions weren’t met.", diff --git a/src/assets/i18n/ua.json b/src/assets/i18n/ua.json index f65f77b0ca..106671412f 100644 --- a/src/assets/i18n/ua.json +++ b/src/assets/i18n/ua.json @@ -518,6 +518,10 @@ "friend-request": "Ви не можете додати цього друга", "friend-already-added": "Цього друга вже було додано", "error-create-address": "Упс! Виникла помилка при створенні адреси. Будь ласка, спробуйте ще раз.", + "habit-not-added": "Неможливо прийняти запрошення", + "habit-already-added": "Неможливо відхилити запрошення", + "habit-added-success": "Запрошення прийнято", + "habit-decline-success": "Запрошення відхилено", "error-edit-address": "Упс! Виникла помилка при редагуванні адреси. Будь ласка, спробуйте ще раз." }, "attention": { From 6468854214436165e28459dd3094181376867340 Mon Sep 17 00:00:00 2001 From: sofiia koval Date: Tue, 19 Nov 2024 14:12:42 +0200 Subject: [PATCH 3/9] fix: add types --- .../user/directives/notific-content-replace.directive.ts | 3 --- src/app/main/service/habit/habit.service.ts | 8 ++++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/app/main/component/user/directives/notific-content-replace.directive.ts b/src/app/main/component/user/directives/notific-content-replace.directive.ts index 2b8d72b0bc..f6f0b7cd61 100644 --- a/src/app/main/component/user/directives/notific-content-replace.directive.ts +++ b/src/app/main/component/user/directives/notific-content-replace.directive.ts @@ -57,11 +57,8 @@ export class NotificContentReplaceDirective implements OnChanges { const linkAttributes = idToNavigate ? { userId: replacements[idToNavigate] } : null; result = this.buildReplacementString(result, contentKey, replacements[replacementKey], linkAttributes); - console.log('in 3'); } }); - - console.log(result); return result; } diff --git a/src/app/main/service/habit/habit.service.ts b/src/app/main/service/habit/habit.service.ts index b00686f422..461a6504e8 100644 --- a/src/app/main/service/habit/habit.service.ts +++ b/src/app/main/service/habit/habit.service.ts @@ -84,12 +84,12 @@ export class HabitService { return this.http.delete(`${habitLink}/delete/${id}`); } - acceptHabitInvitation(invitationId: number) { - return this.http.patch(`${habitLink}/invite/${invitationId}/accept`, {}); + acceptHabitInvitation(invitationId: number): Observable { + return this.http.patch(`${habitLink}/invite/${invitationId}/accept`, {}); } - declineHabitInvitation(invitationId: number) { - return this.http.patch(`${habitLink}/invite/${invitationId}/reject`, {}); + declineHabitInvitation(invitationId: number): Observable { + return this.http.patch(`${habitLink}/invite/${invitationId}/reject`, {}); } private prepareCustomHabitRequest(habit: CustomHabit, lang: string): FormData { From 3cced8f1406c201048f85b9067a0539f322b7b67 Mon Sep 17 00:00:00 2001 From: sofiia koval Date: Tue, 19 Nov 2024 14:34:50 +0200 Subject: [PATCH 4/9] fix: lint --- .../user/directives/notific-content-replace.directive.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/app/main/component/user/directives/notific-content-replace.directive.ts b/src/app/main/component/user/directives/notific-content-replace.directive.ts index f6f0b7cd61..07bdf79dd3 100644 --- a/src/app/main/component/user/directives/notific-content-replace.directive.ts +++ b/src/app/main/component/user/directives/notific-content-replace.directive.ts @@ -47,10 +47,7 @@ export class NotificContentReplaceDirective implements OnChanges { } } else if (contentKey === 'message' && replacements.notificationType) { const linkAttributes = replacements.targetId - ? { - targetId: replacements.targetId, - notificationType: replacements.notificationType - } + ? { targetId: replacements.targetId, notificationType: replacements.notificationType } : null; result = this.buildReplacementString(result, contentKey, replacements[replacementKey], linkAttributes); } else if (replacements.hasOwnProperty(replacementKey)) { From f95388a7e6ab3d2bf8350aae5753df4a4ce55b76 Mon Sep 17 00:00:00 2001 From: sofiia koval Date: Tue, 19 Nov 2024 15:09:08 +0200 Subject: [PATCH 5/9] fix: tests --- .../user-notifications.component.spec.ts | 2 +- .../directives/notific-content-replace.directive.spec.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.spec.ts b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.spec.ts index a0c7fc8dda..9bb506ca18 100644 --- a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.spec.ts +++ b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.spec.ts @@ -181,7 +181,7 @@ describe('UserNotificationsComponent', () => { component.navigate(customEvent); tick(); - expect(routerMock.navigate).toHaveBeenCalledWith(['profile', 1, 'allhabits', 'edithabit', 3]); + expect(routerMock.navigate).toHaveBeenCalledWith(['profile', 1, 'allhabits', 'addhabit', 3]); })); it('should return checkSelectedFilter', () => { diff --git a/src/app/main/component/user/directives/notific-content-replace.directive.spec.ts b/src/app/main/component/user/directives/notific-content-replace.directive.spec.ts index 0998c51f63..ecc4b6e9e6 100644 --- a/src/app/main/component/user/directives/notific-content-replace.directive.spec.ts +++ b/src/app/main/component/user/directives/notific-content-replace.directive.spec.ts @@ -74,7 +74,7 @@ describe('NotificContentReplaceDirective', () => { component.notification = { ...notification, ...{ bodyText: 'commented event {message}' } }; fixture.detectChanges(); expect(paragrEl.textContent).toBe('commented event test message'); - expect(paragrEl.innerHTML).toBe('commented event test message'); + expect(paragrEl.innerHTML).toBe('commented event test message'); }); it('should add property value to the content and anchor tag', () => { @@ -83,7 +83,10 @@ describe('NotificContentReplaceDirective', () => { ...{ bodyText: '{user1},{user2} commented event {message}', actionUserId: [2, 3], actionUserText: ['testUser1', 'testUser2'] } }; fixture.detectChanges(); - expect(paragrEl.textContent).toBe('testUser1,testUser2 commented event test message'); - expect(paragrEl.innerHTML).toBe('testUser1,testUser2 commented event test message'); + expect(paragrEl.textContent).toBe('testUser1,testUser2 commented event test message'); // Text content without tags + expect(paragrEl.innerHTML).toBe( + 'testUser1,testUser2 commented event ' + + 'test message' + ); }); }); From 08d541e72db57da55da43c2da099bab7d96e156c Mon Sep 17 00:00:00 2001 From: sofiia koval Date: Tue, 19 Nov 2024 15:54:09 +0200 Subject: [PATCH 6/9] fix: make fields readonly --- .../user-notifications.component.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts index 72cc43640e..b5b07b0e9a 100644 --- a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts +++ b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts @@ -47,13 +47,13 @@ export class UserNotificationsComponent implements OnInit, OnDestroy { private filterAll = 'All'; constructor( - private localStorageService: LocalStorageService, - public translate: TranslateService, - private userNotificationService: UserNotificationService, - private matSnackBar: MatSnackBarComponent, - private userFriendsService: UserFriendsService, - private router: Router, - private userService: UserService, + private readonly localStorageService: LocalStorageService, + public readonly translate: TranslateService, + private readonly userNotificationService: UserNotificationService, + private readonly matSnackBar: MatSnackBarComponent, + private readonly userFriendsService: UserFriendsService, + private readonly router: Router, + private readonly userService: UserService, private readonly habitService: HabitService ) {} From f53c408413166d22085c592c5f4542822597f218 Mon Sep 17 00:00:00 2001 From: sofiia koval Date: Wed, 20 Nov 2024 11:55:09 +0200 Subject: [PATCH 7/9] feat: add tests for accept and decline requests --- .../user-notifications.component.spec.ts | 72 +++++++++++++++---- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.spec.ts b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.spec.ts index 9bb506ca18..fe773b312c 100644 --- a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.spec.ts +++ b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.spec.ts @@ -1,13 +1,13 @@ -import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { UserNotificationsComponent } from './user-notifications.component'; import { TranslateService, TranslateModule } from '@ngx-translate/core'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { of, BehaviorSubject, throwError } from 'rxjs'; +import { of, BehaviorSubject } from 'rxjs'; import { LocalStorageService } from '@global-service/localstorage/local-storage.service'; import { Language } from 'src/app/main/i18n/Language'; import { PipeTransform, Pipe, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; import { MatSnackBarComponent } from '@global-errors/mat-snack-bar/mat-snack-bar.component'; -import { FilterCriteria, NotificationFilter } from '@global-user/models/notification.model'; +import { FilterCriteria } from '@global-user/models/notification.model'; import { Router } from '@angular/router'; import { UserNotificationService } from '@global-user/services/user-notification.service'; @@ -15,6 +15,8 @@ import { UserService } from '@global-service/user/user.service'; import { LocalizedDatePipe } from 'src/app/shared/localized-date-pipe/localized-date.pipe'; import { RelativeDatePipe } from 'src/app/shared/relative-date.pipe'; import { By } from '@angular/platform-browser'; +import { UserFriendsService } from '@global-user/services/user-friends.service'; +import { HabitService } from '@global-service/habit/habit.service'; @Pipe({ name: 'translate' }) class TranslatePipeMock implements PipeTransform { @@ -50,7 +52,7 @@ describe('UserNotificationsComponent', () => { bodyText: 'test texts1', message: 'test message1', notificationId: 2, - notificationType: '', + notificationType: 'HABIT_INVITE', projectName: 'GreeCity', secondMessage: 'secondMessageTest', secondMessageId: 5, @@ -79,6 +81,16 @@ describe('UserNotificationsComponent', () => { localStorageServiceMock.languageSubject = of('en'); localStorageServiceMock.getUserId = () => 1; + const userFriendsServiceMock = { + acceptRequest: jasmine.createSpy('acceptRequest').and.returnValue(of(null)), + declineRequest: jasmine.createSpy('declineRequest').and.returnValue(of(null)) + }; + + const habitServiceMock = { + acceptHabitInvitation: jasmine.createSpy('acceptHabitInvitation').and.returnValue(of(null)), + declineHabitInvitation: jasmine.createSpy('declineHabitInvitation').and.returnValue(of(null)) + }; + const routerMock = jasmine.createSpyObj('router', ['navigate']); const userNotificationServiceMock = jasmine.createSpyObj('userNotificationService', [ @@ -113,7 +125,9 @@ describe('UserNotificationsComponent', () => { { provide: MatSnackBarComponent, useValue: { openSnackBar: () => {} } }, { provide: Router, useValue: routerMock }, { provide: UserNotificationService, useValue: userNotificationServiceMock }, - { provide: UserService, useValue: { userId: 1 } } + { provide: UserService, useValue: { userId: 1 } }, + { provide: UserFriendsService, useValue: userFriendsServiceMock }, + { provide: HabitService, useValue: habitServiceMock } ], schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA] }).compileComponents(); @@ -140,7 +154,7 @@ describe('UserNotificationsComponent', () => { expect(component.filterCriteriaOptions.find((el) => el.name === FilterCriteria.ORIGIN).isSelected).toBeTrue(); }); - it('should navigate to news page when notification type is ECONEWS', fakeAsync(() => { + it('should navigate to news page when notification type is ECONEWS', waitForAsync(() => { const event = new MouseEvent('click'); const target = document.createElement('div'); target.setAttribute('data-notificationType', 'ECONEWS'); @@ -157,12 +171,11 @@ describe('UserNotificationsComponent', () => { spyOn(component, 'navigate').and.callThrough(); component.navigate(customEvent); - tick(); expect(routerMock.navigate).toHaveBeenCalledWith(['news', 5]); })); - it('should navigate to habit editing page when notification type is HABIT', fakeAsync(() => { + it('should navigate to habit editing page when notification type is HABIT', waitForAsync(() => { const event = new MouseEvent('click'); const target = document.createElement('div'); target.setAttribute('data-notificationType', 'HABIT'); @@ -179,7 +192,6 @@ describe('UserNotificationsComponent', () => { spyOn(component, 'navigate').and.callThrough(); component.navigate(customEvent); - tick(); expect(routerMock.navigate).toHaveBeenCalledWith(['profile', 1, 'allhabits', 'addhabit', 3]); })); @@ -189,26 +201,60 @@ describe('UserNotificationsComponent', () => { expect(component.checkSelectedFilter(FilterCriteria.TYPE)).toBeFalsy(); }); - it('should call declineRequest', fakeAsync(() => { + it('should call declineRequest', waitForAsync(() => { component.notifications = notifications; const spy = spyOn(component, 'declineRequest'); fixture.detectChanges(); const button = fixture.debugElement.query(By.css('.decline-request')); button.triggerEventHandler('click', null); - tick(); expect(spy).toHaveBeenCalled(); })); - it('should call accept request', fakeAsync(() => { + it('should call userFriendsService.declineRequest for a friend request', waitForAsync(() => { + component.notifications = [notifications[0]]; + fixture.detectChanges(); + + component.declineRequest(notifications[0]); + + expect(userFriendsServiceMock.declineRequest).toHaveBeenCalledWith(notifications[0].actionUserId[0]); + })); + + it('should call habitService.declineHabitInvitation for a habit invitation', waitForAsync(() => { + component.notifications = [notifications[1]]; + fixture.detectChanges(); + + component.declineRequest(notifications[1]); + + expect(habitServiceMock.declineHabitInvitation).toHaveBeenCalledWith(notifications[1].secondMessageId); + })); + + it('should call accept request', waitForAsync(() => { component.notifications = notifications; const spy = spyOn(component, 'acceptRequest'); fixture.detectChanges(); const button = fixture.debugElement.query(By.css('.accept-request')); button.triggerEventHandler('click', null); - tick(); expect(spy).toHaveBeenCalled(); })); + it('should call userFriendsService.acceptRequest for a friend request', waitForAsync(() => { + component.notifications = [notifications[0]]; + fixture.detectChanges(); + + component.acceptRequest(notifications[0]); + + expect(userFriendsServiceMock.acceptRequest).toHaveBeenCalledWith(notifications[0].actionUserId[0]); + })); + + it('should call habitService.acceptHabitInvitation for a habit invitation', waitForAsync(() => { + component.notifications = [notifications[1]]; + fixture.detectChanges(); + + component.acceptRequest(notifications[1]); + + expect(habitServiceMock.acceptHabitInvitation).toHaveBeenCalledWith(notifications[1].secondMessageId); + })); + it('onScroll', () => { const spy = spyOn(component, 'getNotification'); component.isLoading = false; From 6750f1b88e7627f52bf23d59292776a188bf873c Mon Sep 17 00:00:00 2001 From: sofiia koval Date: Thu, 21 Nov 2024 17:14:28 +0200 Subject: [PATCH 8/9] fix: comments --- .../mat-snack-bar/mat-snack-bar.component.ts | 4 +- .../user-notifications.component.html | 7 +- .../user-notifications.component.spec.ts | 2 - .../user-notifications.component.ts | 70 ++++++++++++------- src/assets/i18n/en.json | 6 +- src/assets/i18n/ua.json | 6 +- 6 files changed, 53 insertions(+), 42 deletions(-) diff --git a/src/app/main/component/errors/mat-snack-bar/mat-snack-bar.component.ts b/src/app/main/component/errors/mat-snack-bar/mat-snack-bar.component.ts index a6598a455e..9f4c902ebc 100644 --- a/src/app/main/component/errors/mat-snack-bar/mat-snack-bar.component.ts +++ b/src/app/main/component/errors/mat-snack-bar/mat-snack-bar.component.ts @@ -74,8 +74,8 @@ export class MatSnackBarComponent { addFriend: { classname: SnackbarClassName.success, key: 'snack-bar.success.add-friend' }, friendValidation: { classname: SnackbarClassName.error, key: 'snack-bar.error.friend-request' }, friendInValidRequest: { classname: SnackbarClassName.error, key: 'snack-bar.error.friend-already-added' }, - habitAcceptRequest: { classname: SnackbarClassName.success, key: 'snack-bar.error.habit-added-success' }, - habitDeclineRequest: { classname: SnackbarClassName.success, key: 'snack-bar.error.habit-decline-success' }, + habitAcceptRequest: { classname: SnackbarClassName.success, key: 'snack-bar.success.habit-added-success' }, + habitDeclineRequest: { classname: SnackbarClassName.success, key: 'snack-bar.success.habit-decline-success' }, habitAcceptInValidRequest: { classname: SnackbarClassName.error, key: 'snack-bar.error.habit-not-added' }, habitDeclineInValidRequest: { classname: SnackbarClassName.error, key: 'snack-bar.error.habit-already-added' }, cancelRequest: { classname: SnackbarClassName.success, key: 'snack-bar.success.cancel-request' }, diff --git a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.html b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.html index f6b0f27c72..44326311c5 100644 --- a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.html +++ b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.html @@ -109,12 +109,7 @@

{{ 'homepage.notifications.title' | translate }}

(click)="navigate($event)" (keydown)="navigate($event)" >

-
+
diff --git a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.spec.ts b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.spec.ts index fe773b312c..49f084eceb 100644 --- a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.spec.ts +++ b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.spec.ts @@ -155,7 +155,6 @@ describe('UserNotificationsComponent', () => { }); it('should navigate to news page when notification type is ECONEWS', waitForAsync(() => { - const event = new MouseEvent('click'); const target = document.createElement('div'); target.setAttribute('data-notificationType', 'ECONEWS'); target.setAttribute('data-targetid', '5'); @@ -176,7 +175,6 @@ describe('UserNotificationsComponent', () => { })); it('should navigate to habit editing page when notification type is HABIT', waitForAsync(() => { - const event = new MouseEvent('click'); const target = document.createElement('div'); target.setAttribute('data-notificationType', 'HABIT'); target.setAttribute('data-targetid', '3'); diff --git a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts index b5b07b0e9a..aa12f4003e 100644 --- a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts +++ b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts @@ -33,7 +33,6 @@ export class UserNotificationsComponent implements OnInit, OnDestroy { filterCriteriaOptions = filterCriteriaOptions; notificationCriteriaOptions = notificationCriteriaOptions; projects = projects; - notificationTypes = NotificationCriteria; notificationFriendRequest = NotificationCriteria.FRIEND_REQUEST_RECEIVED; notificationHabitInvitation = NotificationCriteria.HABIT_INVITATION; @@ -263,12 +262,35 @@ export class UserNotificationsComponent implements OnInit, OnDestroy { } } + isFriendRequest(notification: NotificationModel): boolean { + return notification.notificationType === this.notificationFriendRequest; + } + + isHabitInvitation(notification: NotificationModel): boolean { + return notification.notificationType === this.notificationHabitInvitation; + } + acceptRequest(notification: NotificationModel): void { - if (notification.notificationType === this.notificationFriendRequest) { - let isAccepted = true; + if (this.isFriendRequest(notification)) { + this.handleFriendRequest(notification, 'accept'); + } else if (this.isHabitInvitation(notification)) { + this.handleHabitInvitation(notification, 'accept'); + } + } + + declineRequest(notification: NotificationModel): void { + if (this.isFriendRequest(notification)) { + this.handleFriendRequest(notification, 'decline'); + } else if (this.isHabitInvitation(notification)) { + this.handleHabitInvitation(notification, 'decline'); + } + } - const userId = notification.actionUserId[0]; + private handleFriendRequest(notification: NotificationModel, action: 'accept' | 'decline'): void { + let isAccepted = true; + const userId = notification.actionUserId[0]; + if (action === 'accept') { this.userFriendsService.acceptRequest(userId).subscribe({ error: () => { isAccepted = false; @@ -279,43 +301,39 @@ export class UserNotificationsComponent implements OnInit, OnDestroy { } } }); - } else if (notification.notificationType === this.notificationHabitInvitation) { - const invitationId = notification.secondMessageId; - - this.habitService.acceptHabitInvitation(invitationId).subscribe({ - next: () => { - this.matSnackBar.openSnackBar('habitAcceptRequest'); - }, + } else { + this.userFriendsService.declineRequest(userId).subscribe({ error: () => { - this.matSnackBar.openSnackBar('habitAcceptInValidRequest'); + isAccepted = false; + }, + complete: () => { + if (isAccepted) { + this.matSnackBar.openSnackBar('friendInValidRequest'); + } } }); } } - declineRequest(notification: NotificationModel): void { + private handleHabitInvitation(notification: NotificationModel, action: 'accept' | 'decline'): void { let isAccepted = true; - const userId = notification.actionUserId[0]; - if (notification.notificationType === this.notificationFriendRequest) { - this.userFriendsService.declineRequest(userId).subscribe({ + const invitationId = notification.secondMessageId; + if (action === 'accept') { + this.habitService.acceptHabitInvitation(invitationId).subscribe({ error: () => { isAccepted = false; }, complete: () => { - if (isAccepted) { - this.matSnackBar.openSnackBar('friendInValidRequest'); - } + this.matSnackBar.openSnackBar(isAccepted ? 'habitAcceptRequest' : 'habitAcceptInValidRequest'); } }); - } else if (notification.notificationType === this.notificationHabitInvitation) { - const invitationId = notification.secondMessageId; - + } else { this.habitService.declineHabitInvitation(invitationId).subscribe({ - next: () => { - this.matSnackBar.openSnackBar('habitDeclineRequest'); - }, error: () => { - this.matSnackBar.openSnackBar('habitDeclineInValidRequest'); + isAccepted = false; + }, + complete: () => { + this.matSnackBar.openSnackBar(isAccepted ? 'habitDeclineRequest' : 'habitDeclineInValidRequest'); } }); } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index c7010bfae7..fe6b4c326b 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -513,8 +513,6 @@ "error-create-address": "Oops, An error occurred while creating the address. Please try again.", "habit-not-added": "This habit invitation can`t be accepted", "habit-already-added": "This habit invitation can`t be declined", - "habit-added-success": "Habit invitation was accepted", - "habit-decline-success": "Habit invitation was declined", "error-edit-address": "Oops, An error occurred while editing the address. Please try again." }, "attention": { @@ -538,7 +536,9 @@ "add-friend": "Friend request sent", "cancel-request": "Your friend request has been canceled", "joint-event-request": "The request to join the closed event has been successfully sent to the organizer", - "create-news": "Your news has been successfully published" + "create-news": "Your news has been successfully published", + "habit-added-success": "Habit invitation was accepted", + "habit-decline-success": "Habit invitation was declined" } }, "user": { diff --git a/src/assets/i18n/ua.json b/src/assets/i18n/ua.json index 106671412f..03fdd4ae59 100644 --- a/src/assets/i18n/ua.json +++ b/src/assets/i18n/ua.json @@ -520,8 +520,6 @@ "error-create-address": "Упс! Виникла помилка при створенні адреси. Будь ласка, спробуйте ще раз.", "habit-not-added": "Неможливо прийняти запрошення", "habit-already-added": "Неможливо відхилити запрошення", - "habit-added-success": "Запрошення прийнято", - "habit-decline-success": "Запрошення відхилено", "error-edit-address": "Упс! Виникла помилка при редагуванні адреси. Будь ласка, спробуйте ще раз." }, "attention": { @@ -547,7 +545,9 @@ "add-friend": "Запит на дружбу надіслано", "cancel-request": "Запит на дружбу скасовано", "joint-event-request": "Запит на приєднання до закритої події надіслано", - "create-news": "Ваша новина успішно опублікована" + "create-news": "Ваша новина успішно опублікована", + "habit-added-success": "Запрошення прийнято", + "habit-decline-success": "Запрошення відхилено" } }, "user": { From a1d3ecc54d76a7001d3298a1ef3aa6cdd630357b Mon Sep 17 00:00:00 2001 From: sofiia koval Date: Fri, 22 Nov 2024 13:11:08 +0200 Subject: [PATCH 9/9] fix: comments --- .../mat-snack-bar/mat-snack-bar.component.ts | 2 ++ .../user-notifications.component.ts | 8 ++---- .../notific-content-replace.directive.spec.ts | 15 ++++------ .../notific-content-replace.directive.ts | 5 ---- .../main/service/habit/habit.service.spec.ts | 28 ++++++++++++++++++- src/app/main/service/habit/habit.service.ts | 2 +- src/assets/i18n/en.json | 6 ++-- src/assets/i18n/ua.json | 6 ++-- 8 files changed, 46 insertions(+), 26 deletions(-) diff --git a/src/app/main/component/errors/mat-snack-bar/mat-snack-bar.component.ts b/src/app/main/component/errors/mat-snack-bar/mat-snack-bar.component.ts index 9f4c902ebc..653f663243 100644 --- a/src/app/main/component/errors/mat-snack-bar/mat-snack-bar.component.ts +++ b/src/app/main/component/errors/mat-snack-bar/mat-snack-bar.component.ts @@ -74,6 +74,8 @@ export class MatSnackBarComponent { addFriend: { classname: SnackbarClassName.success, key: 'snack-bar.success.add-friend' }, friendValidation: { classname: SnackbarClassName.error, key: 'snack-bar.error.friend-request' }, friendInValidRequest: { classname: SnackbarClassName.error, key: 'snack-bar.error.friend-already-added' }, + friendRequestAccepted: { classname: SnackbarClassName.success, key: 'snack-bar.success.friend-added-success' }, + friendRequestDeclined: { classname: SnackbarClassName.success, key: 'snack-bar.success.friend-declined-success' }, habitAcceptRequest: { classname: SnackbarClassName.success, key: 'snack-bar.success.habit-added-success' }, habitDeclineRequest: { classname: SnackbarClassName.success, key: 'snack-bar.success.habit-decline-success' }, habitAcceptInValidRequest: { classname: SnackbarClassName.error, key: 'snack-bar.error.habit-not-added' }, diff --git a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts index aa12f4003e..bc499f7784 100644 --- a/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts +++ b/src/app/main/component/user/components/profile/user-notifications/user-notifications.component.ts @@ -296,9 +296,7 @@ export class UserNotificationsComponent implements OnInit, OnDestroy { isAccepted = false; }, complete: () => { - if (isAccepted) { - this.matSnackBar.openSnackBar('friendInValidRequest'); - } + this.matSnackBar.openSnackBar(isAccepted ? 'friendRequestAccepted' : 'friendInValidRequest'); } }); } else { @@ -307,9 +305,7 @@ export class UserNotificationsComponent implements OnInit, OnDestroy { isAccepted = false; }, complete: () => { - if (isAccepted) { - this.matSnackBar.openSnackBar('friendInValidRequest'); - } + this.matSnackBar.openSnackBar(isAccepted ? 'friendRequestDeclined' : 'friendInValidRequest'); } }); } diff --git a/src/app/main/component/user/directives/notific-content-replace.directive.spec.ts b/src/app/main/component/user/directives/notific-content-replace.directive.spec.ts index ecc4b6e9e6..229b29fe83 100644 --- a/src/app/main/component/user/directives/notific-content-replace.directive.spec.ts +++ b/src/app/main/component/user/directives/notific-content-replace.directive.spec.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { NotificContentReplaceDirective } from './notific-content-replace.directive'; @Component({ @@ -30,7 +30,7 @@ describe('NotificContentReplaceDirective', () => { viewed: false }; - beforeEach(() => { + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [TestComponent, NotificContentReplaceDirective] }); @@ -39,7 +39,7 @@ describe('NotificContentReplaceDirective', () => { fixture.detectChanges(); component = fixture.componentInstance; paragrEl = fixture.nativeElement.querySelector('p'); - }); + })); it('should display multiple user replacements', () => { component.notification = { ...notification, ...{ bodyText: '{user1} and {user2} liked your post' } }; @@ -74,7 +74,7 @@ describe('NotificContentReplaceDirective', () => { component.notification = { ...notification, ...{ bodyText: 'commented event {message}' } }; fixture.detectChanges(); expect(paragrEl.textContent).toBe('commented event test message'); - expect(paragrEl.innerHTML).toBe('commented event test message'); + expect(paragrEl.innerHTML).toBe('commented event test message'); }); it('should add property value to the content and anchor tag', () => { @@ -83,10 +83,7 @@ describe('NotificContentReplaceDirective', () => { ...{ bodyText: '{user1},{user2} commented event {message}', actionUserId: [2, 3], actionUserText: ['testUser1', 'testUser2'] } }; fixture.detectChanges(); - expect(paragrEl.textContent).toBe('testUser1,testUser2 commented event test message'); // Text content without tags - expect(paragrEl.innerHTML).toBe( - 'testUser1,testUser2 commented event ' + - 'test message' - ); + expect(paragrEl.textContent).toBe('testUser1,testUser2 commented event test message'); + expect(paragrEl.innerHTML).toBe('testUser1,testUser2 commented event test message'); }); }); diff --git a/src/app/main/component/user/directives/notific-content-replace.directive.ts b/src/app/main/component/user/directives/notific-content-replace.directive.ts index 07bdf79dd3..a3c2e7e362 100644 --- a/src/app/main/component/user/directives/notific-content-replace.directive.ts +++ b/src/app/main/component/user/directives/notific-content-replace.directive.ts @@ -45,11 +45,6 @@ export class NotificContentReplaceDirective implements OnChanges { userId: replacements.actionUserId[index] }); } - } else if (contentKey === 'message' && replacements.notificationType) { - const linkAttributes = replacements.targetId - ? { targetId: replacements.targetId, notificationType: replacements.notificationType } - : null; - result = this.buildReplacementString(result, contentKey, replacements[replacementKey], linkAttributes); } else if (replacements.hasOwnProperty(replacementKey)) { const linkAttributes = idToNavigate ? { userId: replacements[idToNavigate] } : null; diff --git a/src/app/main/service/habit/habit.service.spec.ts b/src/app/main/service/habit/habit.service.spec.ts index 3d3fab39b8..670312de13 100644 --- a/src/app/main/service/habit/habit.service.spec.ts +++ b/src/app/main/service/habit/habit.service.spec.ts @@ -1,5 +1,5 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; +import { TestBed, waitForAsync } from '@angular/core/testing'; import { LocalStorageService } from '@global-service/localstorage/local-storage.service'; import { BehaviorSubject } from 'rxjs'; import { HabitService } from './habit.service'; @@ -255,4 +255,30 @@ describe('HabitService', () => { expect(req.request.method).toBe('GET'); req.flush(HABITLIST); }); + + it('should accept habit invitation', () => { + const invitationId = 123; + const mockResponse = 'Invitation accepted successfully'; + + habitService.acceptHabitInvitation(invitationId).subscribe((response) => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(`${habitLink}/invite/${invitationId}/accept`); + expect(req.request.method).toBe('PATCH'); + req.flush(mockResponse); + }); + + it('should decline habit invitation', () => { + const invitationId = 456; + const mockResponse = 'Invitation declined successfully'; + + habitService.declineHabitInvitation(invitationId).subscribe((response) => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(`${habitLink}/invite/${invitationId}/reject`); + expect(req.request.method).toBe('DELETE'); + req.flush(mockResponse); + }); }); diff --git a/src/app/main/service/habit/habit.service.ts b/src/app/main/service/habit/habit.service.ts index 461a6504e8..304e2e7019 100644 --- a/src/app/main/service/habit/habit.service.ts +++ b/src/app/main/service/habit/habit.service.ts @@ -89,7 +89,7 @@ export class HabitService { } declineHabitInvitation(invitationId: number): Observable { - return this.http.patch(`${habitLink}/invite/${invitationId}/reject`, {}); + return this.http.delete(`${habitLink}/invite/${invitationId}/reject`); } private prepareCustomHabitRequest(habit: CustomHabit, lang: string): FormData { diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index fe6b4c326b..8afc05583c 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -509,7 +509,7 @@ "exist-address": "This address has already been added", "certificate-duration": "The duration of the certificate cant be more than 12 months", "friend-request": "You cannot add this friend", - "friend-already-added": "This friend has been already added", + "friend-already-added": "This friend invitation has been already accepted or declined", "error-create-address": "Oops, An error occurred while creating the address. Please try again.", "habit-not-added": "This habit invitation can`t be accepted", "habit-already-added": "This habit invitation can`t be declined", @@ -538,7 +538,9 @@ "joint-event-request": "The request to join the closed event has been successfully sent to the organizer", "create-news": "Your news has been successfully published", "habit-added-success": "Habit invitation was accepted", - "habit-decline-success": "Habit invitation was declined" + "habit-decline-success": "Habit invitation was declined", + "friend-added-success": "Friend invitation accepted", + "friend-declined-success": "Friend invitation declined" } }, "user": { diff --git a/src/assets/i18n/ua.json b/src/assets/i18n/ua.json index 03fdd4ae59..6c01adecae 100644 --- a/src/assets/i18n/ua.json +++ b/src/assets/i18n/ua.json @@ -516,7 +516,7 @@ "exist-address": "Ця адреса вже додана", "certificate-duration": "Термін дії сертифіката не може перевищувати 12 місяців", "friend-request": "Ви не можете додати цього друга", - "friend-already-added": "Цього друга вже було додано", + "friend-already-added": "Це запрошення друга вже було додано або відхилено", "error-create-address": "Упс! Виникла помилка при створенні адреси. Будь ласка, спробуйте ще раз.", "habit-not-added": "Неможливо прийняти запрошення", "habit-already-added": "Неможливо відхилити запрошення", @@ -547,7 +547,9 @@ "joint-event-request": "Запит на приєднання до закритої події надіслано", "create-news": "Ваша новина успішно опублікована", "habit-added-success": "Запрошення прийнято", - "habit-decline-success": "Запрошення відхилено" + "habit-decline-success": "Запрошення відхилено", + "friend-added-success": "Запит на дружбу прийнято", + "friend-declined-success": "Запит на дружбу відхилено" } }, "user": {