@chat21/chat21-ionic 3.4.32-rc1 → 3.4.32-rc10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { Component, ElementRef, EventEmitter, HostListener, Input, OnChanges, OnInit, Output } from '@angular/core';
1
+ import { Component, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnChanges, OnInit, Output } from '@angular/core';
2
2
  import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
3
3
  import { LoggerService } from 'src/chat21-core/providers/abstract/logger.service';
4
4
  import { TranslateService } from '@ngx-translate/core';
@@ -17,12 +17,14 @@ import { Project } from 'src/chat21-core/models/projects';
17
17
  import { BRAND_BASE_INFO } from 'src/app/utils/utils-resources';
18
18
  import { getOSCode } from 'src/app/utils/utils';
19
19
  import { getUserStatusFromProjectUser } from 'src/chat21-core/utils/utils';
20
+ import { ProjectService } from 'src/app/services/projects/project.service';
21
+ import { ProjectUser } from 'src/chat21-core/models/project_user';
20
22
  @Component({
21
23
  selector: 'app-sidebar-user-details',
22
24
  templateUrl: './sidebar-user-details.component.html',
23
25
  styleUrls: ['./sidebar-user-details.component.scss'],
24
26
  })
25
- export class SidebarUserDetailsComponent implements OnInit, OnChanges {
27
+ export class SidebarUserDetailsComponent implements OnInit, OnChanges, OnDestroy {
26
28
  // HAS_CLICKED_OPEN_USER_DETAIL: boolean = false;
27
29
  // @Output() onCloseUserDetailsSidebar = new EventEmitter();
28
30
 
@@ -55,6 +57,15 @@ export class SidebarUserDetailsComponent implements OnInit, OnChanges {
55
57
  selectedStatus: any;
56
58
  TEAMMATE_STATUS = TEAMMATE_STATUS;
57
59
 
60
+ projects: ProjectUser[] = [];
61
+ selectedProjectForStatus: ProjectUser | null = null;
62
+ public openDropdownProjects: boolean = false
63
+ public openStatusDropdownProjectId: string | null = null
64
+ statusDropdownPosition = { top: 0, left: 0 };
65
+ isVisibleMT = false;
66
+ isVisibleMPA = false;
67
+ private userDetailsMutationObserver: MutationObserver | null = null;
68
+ private statusDropdownCloseTimeout: any = null;
58
69
 
59
70
  translationsMap: Map<string, string> = new Map();
60
71
 
@@ -71,7 +82,7 @@ export class SidebarUserDetailsComponent implements OnInit, OnChanges {
71
82
  public appConfigProvider: AppConfigProvider,
72
83
  public events: EventsService,
73
84
  private eRef: ElementRef,
74
-
85
+ private projectService: ProjectService,
75
86
  ) { }
76
87
 
77
88
  ngOnInit() {
@@ -80,11 +91,47 @@ export class SidebarUserDetailsComponent implements OnInit, OnChanges {
80
91
  this.subcribeToAuthStateChanged();
81
92
  this.listenTocurrentProjectUserUserAvailability$();
82
93
  this.listenToCurrentStoredProject();
94
+ this.listenToUserGoOnline();
83
95
  this.getOSCODE();
96
+ this.setupUserDetailsCloseObserver();
84
97
  }
85
98
 
86
99
  ngOnChanges() { }
87
100
 
101
+ ngOnDestroy(): void {
102
+ this.userDetailsMutationObserver?.disconnect();
103
+ this.userDetailsMutationObserver = null;
104
+ this.cancelStatusDropdownClose();
105
+ }
106
+
107
+ /**
108
+ * Osserva la rimozione della classe 'active' da #user-details (es. chiusura via click avatar nel sidebar)
109
+ * per chiudere i dropdown aperti
110
+ */
111
+ private setupUserDetailsCloseObserver(): void {
112
+ setTimeout(() => {
113
+ const el = document.getElementById('user-details');
114
+ if (!el) return;
115
+ this.userDetailsMutationObserver = new MutationObserver((mutations) => {
116
+ mutations.forEach((mutation) => {
117
+ if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
118
+ const target = mutation.target as HTMLElement;
119
+ if (!target.classList.contains('active')) {
120
+ this.closeDropdowns();
121
+ }
122
+ }
123
+ });
124
+ });
125
+ this.userDetailsMutationObserver.observe(el, { attributes: true, attributeFilter: ['class'] });
126
+ }, 0);
127
+ }
128
+
129
+ private closeDropdowns(): void {
130
+ this.openDropdownProjects = false;
131
+ this.openStatusDropdownProjectId = null;
132
+ this.selectedProjectForStatus = null;
133
+ }
134
+
88
135
  subcribeToAuthStateChanged() {
89
136
  this.messagingAuthService.BSAuthStateChanged.subscribe((state) => {
90
137
  this.logger.log('[SIDEBAR-USER-DETAILS] BSAuthStateChanged ', state)
@@ -240,6 +287,8 @@ export class SidebarUserDetailsComponent implements OnInit, OnChanges {
240
287
  .set('LABEL_LOGOUT', text['LABEL_LOGOUT'])
241
288
  .set('SubscriptionPaymentProblem', text['SubscriptionPaymentProblem'])
242
289
  .set('ThePlanHasExpired', text['ThePlanHasExpired'])
290
+ .set('NAVBAR.RECENT_PROJECTS', text['NAVBAR.RECENT_PROJECTS'])
291
+ .set('NAVBAR.OTHER_PROJECTS', text['NAVBAR.OTHER_PROJECTS'])
243
292
 
244
293
  this.TEAMMATE_STATUS.forEach(element => {
245
294
  element.label = this.translationsMap.get(element.label)
@@ -255,6 +304,36 @@ export class SidebarUserDetailsComponent implements OnInit, OnChanges {
255
304
  this.logger.log('[SIDEBAR-USER-DETAILS] AppConfigService getAppConfig', this.appConfigProvider.getConfig());
256
305
 
257
306
  this.isVisiblePAY = getOSCode("PAY", this.public_Key);
307
+ this.isVisibleMT = getOSCode("MTT", this.public_Key);
308
+ this.isVisibleMPA = getOSCode("MPA", this.public_Key);
309
+ }
310
+
311
+ listenToUserGoOnline() {
312
+ this.events.subscribe('go:online', (isOnline: boolean) => {
313
+ this.logger.log('[SIDEBAR-USER-DETAILS] listen to go:online --> ', isOnline);
314
+ if (isOnline) {
315
+ this.tiledeskToken = this.tiledeskAuthService.getTiledeskToken();
316
+ this.getProjects();
317
+ }
318
+ });
319
+ }
320
+
321
+ getProjects() {
322
+ this.logger.log('[SIDEBAR-USER-DETAILS] calling getProjects ... ');
323
+ this.projectService.getProjects().subscribe((projects: ProjectUser[]) => {
324
+ this.logger.log('[SIDEBAR-USER-DETAILS] getProjects PROJECTS ', projects);
325
+ if (projects) {
326
+ this.projects = projects.filter((prj: ProjectUser) => prj?.id_project?.status === 100);
327
+ this.projects.forEach((prj: ProjectUser) => {
328
+ prj.teammateStatus = getUserStatusFromProjectUser(prj as any);
329
+ });
330
+ this.logger.log('[SIDEBAR-USER-DETAILS] getProjects this.projects ', this.projects);
331
+ }
332
+ }, (error) => {
333
+ this.logger.error('[SIDEBAR-USER-DETAILS] getProjects - ERROR ', error);
334
+ }, () => {
335
+ this.logger.log('[SIDEBAR-USER-DETAILS] getProjects - COMPLETE');
336
+ });
258
337
  }
259
338
 
260
339
  listenToCurrentStoredProject() {
@@ -268,9 +347,9 @@ export class SidebarUserDetailsComponent implements OnInit, OnChanges {
268
347
  name: projectObjct['id_project']['name'],
269
348
  profile: projectObjct['id_project']['profile'],
270
349
  isActiveSubscription: projectObjct['id_project']['isActiveSubscription'],
271
- trialExpired: projectObjct['id_project']['trialExpired']
350
+ trialExpired: projectObjct['id_project']['trialExpired'],
351
+ teammateStatus: getUserStatusFromProjectUser(projectObjct as any)
272
352
  }
273
-
274
353
  if (this.project.profile.type === 'free') {
275
354
 
276
355
  if (this.project.trialExpired === false) {
@@ -285,11 +364,17 @@ export class SidebarUserDetailsComponent implements OnInit, OnChanges {
285
364
  }
286
365
 
287
366
  this.wsService.subscriptionToWsCurrentProjectUserAvailability(this.project._id, projectObjct._id);
367
+ if (this.tiledeskToken) {
368
+ this.getProjects();
369
+ }
288
370
  }
289
371
  })
290
372
 
291
373
  try {
292
374
  this.tiledeskToken = this.appStorageService.getItem('tiledeskToken');
375
+ if (this.tiledeskToken) {
376
+ this.getProjects();
377
+ }
293
378
  // this.logger.log('[SIDEBAR-USER-DETAILS] - GET STORED TOKEN ', this.tiledeskToken)
294
379
  } catch (err) {
295
380
  this.logger.error('[SIDEBAR-USER-DETAILS] - GET STORED TOKEN ', err)
@@ -350,6 +435,118 @@ export class SidebarUserDetailsComponent implements OnInit, OnChanges {
350
435
  });
351
436
  }
352
437
 
438
+ getCurrentStatusAvatar(): string {
439
+ const status = this.TEAMMATE_STATUS?.find(s => s.id === this.selectedStatus);
440
+ return status?.avatar || 'assets/img/teammate-status/avaible.svg';
441
+ }
442
+
443
+ getCurrentStatusLabel(): string {
444
+ const status = this.TEAMMATE_STATUS?.find(s => s.id === this.selectedStatus);
445
+ return status?.label || status?.name || '';
446
+ }
447
+
448
+ toggleProjectsDropdown() {
449
+ this.openDropdownProjects = !this.openDropdownProjects;
450
+ if (!this.openDropdownProjects) {
451
+ this.openStatusDropdownProjectId = null;
452
+ this.selectedProjectForStatus = null;
453
+ }
454
+ }
455
+
456
+ openStatusDropdownOnHover(event: Event, prjct: any) {
457
+ this.cancelStatusDropdownClose();
458
+ const projectId = prjct?.id_project?._id;
459
+ const el = event.currentTarget as HTMLElement;
460
+ const rect = el.getBoundingClientRect();
461
+ this.statusDropdownPosition = {
462
+ top: rect.top,
463
+ left: rect.right + 10
464
+ };
465
+ this.selectedProjectForStatus = prjct;
466
+ this.openStatusDropdownProjectId = projectId;
467
+ }
468
+
469
+ closeStatusDropdownOnLeave() {
470
+ this.cancelStatusDropdownClose();
471
+ this.statusDropdownCloseTimeout = setTimeout(() => {
472
+ this.closeDropdowns();
473
+ this.statusDropdownCloseTimeout = null;
474
+ }, 150);
475
+ }
476
+
477
+ cancelStatusDropdownClose() {
478
+ if (this.statusDropdownCloseTimeout) {
479
+ clearTimeout(this.statusDropdownCloseTimeout);
480
+ this.statusDropdownCloseTimeout = null;
481
+ }
482
+ }
483
+
484
+ onChangeProjectStatus(projectUser: ProjectUser, selectedStatusID: any) {
485
+ this.logger.log('[SIDEBAR-USER-DETAILS] onChangeProjectStatus', projectUser, selectedStatusID)
486
+ this.openStatusDropdownProjectId = null
487
+ this.selectedProjectForStatus = null
488
+
489
+ let IS_AVAILABLE = null
490
+ let profilestatus = ''
491
+ if (selectedStatusID === 1) {
492
+ IS_AVAILABLE = true
493
+ } else if (selectedStatusID === 2) {
494
+ IS_AVAILABLE = false
495
+ } else if (selectedStatusID === 3) {
496
+ IS_AVAILABLE = false
497
+ profilestatus = 'inactive'
498
+ }
499
+
500
+ this.wsService.updateCurrentUserAvailability(this.tiledeskToken, projectUser.id_project._id, IS_AVAILABLE, profilestatus).subscribe((projectUserUpdated: any) => {
501
+
502
+ this.logger.log('[NAVBAR] - PROJECT-USER UPDATED ', projectUser)
503
+ this.projects.find(p => p.id_project._id === projectUser.id_project._id).teammateStatus = getUserStatusFromProjectUser(projectUserUpdated as any);
504
+
505
+ if(projectUser.id_project._id === this.project._id) {
506
+ this.project.teammateStatus = getUserStatusFromProjectUser(projectUserUpdated as any);
507
+ }
508
+ }, (error) => {
509
+ this.logger.error('[NAVBAR] - PROJECT-USER UPDATED - ERROR ', error);
510
+
511
+ }, () => {
512
+ this.logger.log('[NAVBAR] - PROJECT-USER UPDATED * COMPLETE *');
513
+
514
+ });
515
+ }
516
+
517
+ onStatusDropdownOptionClick(status: { id: number; name: string; avatar: string; label: string }, projectUser: ProjectUser | null) {
518
+ if (!projectUser) return;
519
+ this.changeProjectStatus(projectUser, status.id);
520
+ this.openStatusDropdownProjectId = null;
521
+ this.selectedProjectForStatus = null;
522
+ if (projectUser?.id_project?._id === this.project?._id) {
523
+ this.selectedStatus = status.id;
524
+ }
525
+ }
526
+
527
+ changeProjectStatus(projectUser: ProjectUser, selectedStatusID: number) {
528
+ this.logger.log('[SIDEBAR-USER-DETAILS] changeProjectStatus projectid', projectUser?.id_project?._id, ' status: ', selectedStatusID);
529
+ let IS_AVAILABLE: boolean | null = null;
530
+ let profilestatus = '';
531
+ if (selectedStatusID === 1) {
532
+ IS_AVAILABLE = true;
533
+ } else if (selectedStatusID === 2) {
534
+ IS_AVAILABLE = false;
535
+ } else if (selectedStatusID === 3) {
536
+ IS_AVAILABLE = false;
537
+ profilestatus = 'inactive';
538
+ }
539
+ this.wsService.updateCurrentUserAvailability(this.tiledeskToken, projectUser.id_project._id, IS_AVAILABLE, profilestatus).subscribe((updated: any) => {
540
+ this.logger.log('[SIDEBAR-USER-DETAILS] - PROJECT-USER UPDATED ', updated);
541
+ const p = this.projects.find(prj => prj?.id_project?._id === projectUser?.id_project?._id);
542
+ if (p) {
543
+ p.teammateStatus = getUserStatusFromProjectUser(updated as any);
544
+ }
545
+ }, (error) => {
546
+ this.logger.error('[SIDEBAR-USER-DETAILS] - PROJECT-USER UPDATED - ERROR ', error);
547
+ });
548
+ }
549
+
353
550
  changeAvailabilityStateInUserDetailsSidebar(selectedStatusID) {
354
551
  this.logger.log('[SIDEBAR-USER-DETAILS] - changeAvailabilityState projectid', this.project._id, ' available 1: ', selectedStatusID);
355
552
 
@@ -57,6 +57,9 @@ import { getOSCode, hasRole } from 'src/app/utils/utils';
57
57
  import { PERMISSIONS } from 'src/app/utils/permissions.constants';
58
58
  import { ProjectUser } from 'src/chat21-core/models/projectUsers';
59
59
  import { ProjectUsersService } from 'src/app/services/project_users/project-users.service';
60
+ import { ProjectService } from 'src/app/services/projects/project.service';
61
+
62
+ import { PROJECTS_STORAGE_KEY } from 'src/chat21-core/utils/constants';
60
63
 
61
64
  @Component({
62
65
  selector: 'app-conversations-list',
@@ -138,10 +141,13 @@ export class ConversationListPage implements OnInit {
138
141
  public tiledeskService: TiledeskService,
139
142
  public tiledeskAuthService: TiledeskAuthService,
140
143
  public projectUsersService: ProjectUsersService,
144
+ public projectService: ProjectService,
141
145
  public appConfigProvider: AppConfigProvider,
142
146
  public platform: Platform,
143
147
  public wsService: WebsocketService,
144
148
  public g: Globals,
149
+ public appStorageService: AppStorageService,
150
+ private triggerEvents: TriggerEvents,
145
151
  ) {
146
152
  this.checkPlatform();
147
153
  this.translations();
@@ -215,8 +221,45 @@ export class ConversationListPage implements OnInit {
215
221
  // @ Lifehooks
216
222
  // -----------------------------------------------
217
223
  ngOnInit() {
218
- this.getAppConfigToHideDiplayBtns()
224
+ this.getAppConfigToHideDiplayBtns();
219
225
  this.getOSCODE();
226
+ this.loadAndStoreProjects();
227
+ }
228
+
229
+ /**
230
+ * Recupera tutti i progetti con getProjects e li salva in AppStorage.
231
+ * Prima di salvare verifica che la chiave non esista già e che non contenga già ogni singolo progetto.
232
+ */
233
+ private loadAndStoreProjects() {
234
+ const token = this.tiledeskAuthService.getTiledeskToken();
235
+ if (!token) return;
236
+ this.projectService.getProjects().subscribe((projects: Project[]) => {
237
+ if (!projects?.length) return;
238
+ let projectsMap: Record<string, Project> = {};
239
+ const stored = this.appStorageService.getItem(PROJECTS_STORAGE_KEY);
240
+ if (stored) {
241
+ try {
242
+ projectsMap = JSON.parse(stored) || {};
243
+ } catch (e) {
244
+ this.logger.warn('[CONVS-LIST-PAGE] loadAndStoreProjects - failed to parse stored projects', e);
245
+ }
246
+ }
247
+ let hasChanges = false;
248
+ projects.forEach((project) => {
249
+ const projectId = project.id_project?._id || project.id_project?.id;
250
+ if (!projectId) return;
251
+ if (!projectsMap[projectId]) {
252
+ projectsMap[projectId] = project.id_project;
253
+ hasChanges = true;
254
+ }
255
+ });
256
+ if (hasChanges) {
257
+ this.appStorageService.setItem(PROJECTS_STORAGE_KEY, JSON.stringify(projectsMap));
258
+ this.logger.log('[CONVS-LIST-PAGE] loadAndStoreProjects - saved', Object.keys(projectsMap).length, 'projects');
259
+ }
260
+ },
261
+ (err) => this.logger.error('[CONVS-LIST-PAGE] loadAndStoreProjects - error', err)
262
+ );
220
263
  }
221
264
 
222
265
  ngOnChanges() {
@@ -795,6 +838,7 @@ export class ConversationListPage implements OnInit {
795
838
  this.logger.log('[CONVS-LIST-PAGE] onConversationSelected active conversation.uid ', conversation.uid)
796
839
  this.events.publish('convList:onConversationSelected', conversation)
797
840
  }
841
+ this.triggerEvents.triggerOnConversationChanged(conversation)
798
842
  }
799
843
 
800
844
  onImageLoaded(conversation: any) {
@@ -862,22 +906,32 @@ export class ConversationListPage implements OnInit {
862
906
  }
863
907
  }
864
908
 
865
- if(conversation.attributes && conversation.attributes['projectId']){
866
- let project = localStorage.getItem(conversation.attributes['projectId'])
867
- if(project){
868
- project = JSON.parse(project)
869
- conversation.attributes.project_name = project['name']
870
- }
871
- }else if(conversation.attributes){
872
- const projectId = getProjectIdSelectedConversation(conversation.uid)
873
- let project = localStorage.getItem(projectId)
874
- if(project){
875
- project = JSON.parse(project)
876
- conversation.attributes.projectId = project['_id']
877
- conversation.attributes.project_name = project['name']
878
- }
909
+ const project = this.getProjectFromStorage(conversation);
910
+ if (project) {
911
+ if (!conversation.attributes) conversation.attributes = {};
912
+ conversation.attributes.projectId = project._id;
913
+ conversation.attributes.project_name = project.name;
879
914
  }
915
+ }
880
916
 
917
+ /** Recupera il progetto dalla chiave di storage (all_projects) */
918
+ private getProjectFromStorage(conversation: ConversationModel): Project | null {
919
+ let projectId: string | undefined;
920
+ if (conversation.attributes?.['projectId']) {
921
+ projectId = conversation.attributes['projectId'];
922
+ } else if (conversation.attributes) {
923
+ projectId = getProjectIdSelectedConversation(conversation.uid);
924
+ }
925
+ if (!projectId) return null;
926
+ const stored = this.appStorageService.getItem(PROJECTS_STORAGE_KEY);
927
+ this.logger.log('[CONVS-LIST-PAGE] getProjectFromStorage - stored', stored);
928
+ if (!stored) return null;
929
+ try {
930
+ const projectsMap: Record<string, Project> = JSON.parse(stored);
931
+ return projectsMap[projectId] || null;
932
+ } catch {
933
+ return null;
934
+ }
881
935
  }
882
936
 
883
937
  // isMarkdownLink(last_message_text) {
@@ -40,11 +40,13 @@
40
40
  </ion-list-conversations>
41
41
  </ng-container>
42
42
  <ng-template #noConvs>
43
- <ion-item id="no-convs" class="ion-text-center" lines="none">
44
- <ion-label class="ion-text-wrap" color="medium">
45
- {{ 'LABEL_MSG_PUSH_START_CHAT' | translate }}
46
- </ion-label>
47
- </ion-item>
43
+ <div class="no-convs-container">
44
+ <ion-item id="no-convs" class="ion-text-center" lines="none">
45
+ <ion-label class="ion-text-wrap" color="medium">
46
+ {{ 'LABEL_MSG_PUSH_START_CHAT' | translate }}
47
+ </ion-label>
48
+ </ion-item>
49
+ </div>
48
50
  </ng-template>
49
51
  </ion-list>
50
52
  </div>
@@ -233,3 +233,18 @@ ion-content::part(scroll) {
233
233
  min-height: calc(100vh - var(--header-height, 56px));
234
234
  height: 100%;
235
235
  }
236
+
237
+ // -------------------------------------------------
238
+ // Empty state - label centrata nell'altezza disponibile
239
+ // -------------------------------------------------
240
+ .no-convs-container {
241
+ display: flex;
242
+ align-items: center;
243
+ justify-content: center;
244
+ min-height: calc(100vh - var(--header-height, 56px));
245
+ width: 100%;
246
+
247
+ ion-item {
248
+ --background: transparent;
249
+ }
250
+ }