Hi. I need to handle a new design for my angular chat architecture. I got two components: private chat and group chat. Both has the inner-chat has a child, which is the component that render the messages. Help me refactor from scratch the code to achiede a supreme software design: import {
  ChangeDetectorRef,
  Component,
  HostBinding,
  OnInit,
  ViewChild,
  OnDestroy,
  AfterViewChecked,
} from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { ModalComponent, ModalConfig } from 'src/app/_metronic/partials';
import { ChatService } from '../../../../services/Api/chat.service';
import { ChatRequest } from '../model/ChatRequest';
import { AuthService, UserType } from 'src/app/modules/auth';
import { BehaviorSubject, Subscription } from 'rxjs';
import { MatchService } from 'src/app/services/Api/match.service';
import { ChatProfile } from '../model/ChatProfile';
import { PageInfoService } from 'src/app/_metronic/layout';
import { catchError, tap } from 'rxjs/operators';
import { Chat } from '../model/Chat';
import { SweetAlertOptions } from 'sweetalert2';
import { KTHelpers } from 'src/app/_metronic/kt';

@Component({
  selector: 'app-private-chat',
  templateUrl: './private-chat.component.html',
  styleUrls: ['./private-chat.component.scss'],
})
export class PrivateChatComponent
  implements OnInit, OnDestroy, AfterViewChecked
{
  @HostBinding('class') class = 'd-flex flex-column flex-lg-row';

  people: ChatProfile[] = [];
  chats: Chat[] = [];
  userSuggestions$ = new BehaviorSubject<ChatProfile[]>([]);
  searchTerm = '';
  currentChat: ChatProfile;
  profile: UserType;
  @ViewChild('modal') modalComponent: ModalComponent;
  swalOptions: SweetAlertOptions = { buttonsStyling: false };

  modalConfig: ModalConfig = {
    modalTitle: 'Availability',
    dismissButtonLabel: "Let's collaborate!",
    closeButtonLabel: 'Cancel',
  };

  form: FormGroup;
  private subscriptions = new Subscription();
  private participantChatMap: { [key: number]: Chat } = {};

  constructor(
    private fb: FormBuilder,
    private chatService: ChatService,
    private authService: AuthService,
    private matchService: MatchService,
    private chRef: ChangeDetectorRef,
    private pageService: PageInfoService
  ) {}

  ngOnInit(): void {
    this.profile = this.authService.currentUserValue;
    this.initForm();
    this.loadChatList();
    this.subscribeToSelectedChat();
  }

  ngOnDestroy(): void {
    this.subscriptions.unsubscribe();
  }

  ngAfterViewChecked(): void {
    KTHelpers.menuReinitialization();
  }

  private loadChatList(): void {
    this.subscriptions.add(
      this.chatService
        .getChatList()
        .pipe(
          tap((res) => this.handleChatListResponse(res)),
          catchError((error) => {
            console.error('Failed to load chat list', error);
            return [];
          })
        )
        .subscribe()
    );
  }

  private handleChatListResponse(res: Chat[]): void {
    if (res.length > 0) {
      this.chats = res.filter((chat) => !chat.isGroup);
      this.people = this.chats.map((chat) => this.mapChatToChatProfile(chat));
      this.populateParticipantChatMap();
      this.selectInitialPerson();
      this.chRef.detectChanges();
    } else {
      this.loadUserSuggestions();
      this.chRef.detectChanges();
    }
  }

  private populateParticipantChatMap(): void {
    this.chats.forEach((chat) => {
      chat.chatParticipants.forEach((participant) => {
        if (participant?.id) {
          this.participantChatMap[participant.id] = chat;
        }
      });
    });
  }

  private selectInitialPerson(): void {
    if (this.people.length > 0) {
      this.selectPerson(this.people[0]);
      this.chatService.selectChat(this.chats[0]);
    } else {
      this.loadUserSuggestions();
    }
  }

  private loadUserSuggestions(): void {
    this.subscriptions.add(
      this.matchService
        .getUserSuggestions()
        .pipe(
          tap((res) => this.userSuggestions$.next(res)),
          catchError((error) => {
            console.error('Failed to load user suggestions', error);
            return [];
          })
        )
        .subscribe()
    );
  }

  initForm() {
    this.form = this.fb.group({
      collaborationTime: [2], // Default collaboration time is 2 hours
    });
  }

  async openModal() {
    return await this.modalComponent.open();
  }

  get filteredPeople(): ChatProfile[] {
    if (!this.searchTerm) {
      return this.people;
    }
    return this.people.filter((person) =>
      (
        person?.firstName.toLowerCase() + person?.lastName.toLowerCase()
      ).includes(this.searchTerm.toLowerCase())
    );
  }

  selectPerson(person: ChatProfile) {
    this.currentChat = person;
    const selectedChat = this.participantChatMap[person.id];
    if (selectedChat) {
      this.chatService.selectChat(selectedChat);
    } else {
      this.createTemporaryChat(person);
    }
    this.chRef.detectChanges();
  }

  private createTemporaryChat(person: ChatProfile): void {
    const tempChat: Partial<Chat> = {
      id: -1,
      isGroup: false,
      chatParticipants: [
        { id: person.id } as Partial<UserType>,
        { id: this.profile?.id } as Partial<UserType>,
      ] as Partial<UserType[]>,
    };
    this.chatService.selectChat(tempChat as Chat);
  }

  plusTime() {
    this.form
      .get('collaborationTime')
      ?.setValue((this.form.get('collaborationTime')?.value ?? 2) + 1);
  }

  minusTime() {
    const currentValue = this.form.get('collaborationTime')?.value ?? 2;
    if (currentValue > 2) {
      this.form.get('collaborationTime')?.setValue(currentValue - 1);
    }
  }

  private subscribeToSelectedChat(): void {
    this.subscriptions.add(
      this.chatService.selectedChat$
        .pipe(
          tap((chat) => this.handleSelectedChat(chat)),
          catchError((error) => {
            console.error('Failed to subscribe to selected chat', error);
            return [];
          })
        )
        .subscribe()
    );
  }

  private handleSelectedChat(chat: Chat | null): void {
    if (chat && chat.id !== -1) {
      this.currentChat = this.mapChatToChatProfile(chat);
      this.chRef.detectChanges();
    }
  }

  private mapChatToChatProfile(chat: Chat): ChatProfile {
    const participant = chat.chatParticipants.find(
      (p) => p?.id !== this.profile?.id
    );
    return {
      id: participant?.id ?? -1,
      firstName: participant?.firstName ?? '',
      lastName: participant?.lastName ?? '',
      photo: participant?.photo ?? '',
      email: participant?.email ?? '',
      lastSeen: '',
      status: '',
    };
  }

  deleteChat() {
    if (this.currentChat) {
      const selectedChat = this.participantChatMap[this.currentChat.id];
      if (selectedChat) {
        this.chatService.deleteChat(selectedChat.id).subscribe();
      }
    }
  }
}
import {
  AfterViewChecked,
  AfterViewInit,
  ChangeDetectorRef,
  Component,
  HostBinding,
  OnInit,
  ViewChild,
  OnDestroy,
} from '@angular/core';
import { ChatService } from 'src/app/services/Api/chat.service';
import { Chat } from '../model/Chat';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AccountService } from 'src/app/services/Api/account.service';
import { AuthService, UserType } from 'src/app/modules/auth';
import { ChatRequest } from '../model/ChatRequest';
import { SwalComponent } from '@sweetalert2/ngx-sweetalert2';
import { SweetAlertOptions } from 'sweetalert2';
import { AddUserGroup } from '../model/AddUserGroup';
import { ModalComponent, ModalConfig } from 'src/app/_metronic/partials';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { KTHelpers } from 'src/app/_metronic/kt';

@Component({
  selector: 'app-group-chat',
  templateUrl: './group-chat.component.html',
  styleUrls: ['./group-chat.component.scss'],
})
export class GroupChatComponent
  implements OnInit, AfterViewInit, AfterViewChecked, OnDestroy
{
  @HostBinding('class') class = 'd-flex flex-column flex-lg-row';
  @ViewChild('createGroupModal') createGroupModal: ModalComponent;
  @ViewChild('addUserModal') addUserModal: ModalComponent;
  @ViewChild('editGroupModal') editGroupModal: ModalComponent;
  @ViewChild('deleteSwal') deleteSwal!: SwalComponent;
  @ViewChild('removeMemberSwal') removeMemberSwal!: SwalComponent;

  groups: Chat[] = [];
  groupPreview: string[] = [];
  groupLength: number = 0;
  searchTerm = '';
  currentChat: Chat | null;
  form: FormGroup;
  editedForm: FormGroup;
  profile: UserType;
  collaborators: UserType[];
  imageSrc: string | ArrayBuffer = 'assets/media/svg/avatars/001-boy.svg'; // Default image path
  editedGroupImage: string | ArrayBuffer = '';


  swalOptions: SweetAlertOptions = { buttonsStyling: false };

  modalConfig: ModalConfig = {
    modalTitle: 'Create Group',
    dismissButtonLabel: 'Cancel',
    closeButtonLabel: 'Create',
  };

  private subscriptions: Subscription[] = [];

  constructor(
    private chatService: ChatService,
    private chRef: ChangeDetectorRef,
    private fb: FormBuilder,
    private accountService: AccountService,
    private authService: AuthService
  ) {}

  ngOnInit(): void {
    this.profile = this.authService.currentUserValue;
    this.initForm();
    this.initEditedForm();
    this.loadChatGroups();
    this.loadCollaborators();
    this.subscribeToSelectedChat();
  }

  ngAfterViewInit(): void {}

  ngAfterViewChecked(): void {
    KTHelpers.menuReinitialization();
  }

  ngOnDestroy(): void {
    this.subscriptions.forEach((sub) => sub.unsubscribe());
  }
  //FIXME: Quando cancello la chat, devo resettare il render, al momento rimane tutto uguale -> isgroup restituisce 0
  private loadChatGroups(): void {
    this.subscriptions.push(
      this.chatService
        .getChatList()
        .pipe(
          tap((res) => {
            if (res.length > 0) {
              this.groups = res.filter((chat) => chat.isGroup);
              const chatFound = this.groups.find(
                (group) => this.currentChat && group.id === this.currentChat.id
              );
              this.selectGroup(chatFound || this.groups[0]);
            } else {
              this.selectGroup(null);
            }
          }),
          catchError((error) => {
            console.error('Failed to load chat groups', error);
            return [];
          })
        )
        .subscribe()
    );
  }

  private loadCollaborators(): void {
    this.subscriptions.push(
      this.accountService
        .getCollaborators()
        .pipe(
          tap((res) => {
            this.collaborators = res;
            this.chRef.detectChanges();
          }),
          catchError((error) => {
            console.error('Failed to load collaborators', error);
            return [];
          })
        )
        .subscribe()
    );
  }

  private initForm(): void {
    this.form = this.fb.group({
      photo: ['', [Validators.required]],
      name: ['', [Validators.required]],
      partecipantsId: [[this.profile?.id], [Validators.required]],
    });
  }

  private initEditedForm(): void {
    this.editedForm = this.fb.group({
      photo: [this.currentChat?.image, [Validators.required]],
      name: [this.currentChat?.name, [Validators.required]],
      partecipantsId: [
        this.currentChat?.chatParticipants.map((participant) => participant?.id),
        [Validators.required],
      ],
    });
    console.log(this.editedForm.value.partecipantsId);
  }

  selectGroup(group: Chat | null): void {
    this.currentChat = group;
    this.chatService.selectChat(group);
    if (this.currentChat) {
      this.groupPreview = this.currentChat.chatParticipants.map(
        (participant: UserType) => participant!.photo
      );
    } else {
      this.groupPreview = [];
    }
    this.chRef.detectChanges();
  }

  //FIXME: Introdurre loading prima di chiusura modale
  createGroup(): void {
    const group: Chat = {
      id: -1,
      name: this.form.value.name,
      chatParticipants: this.form.value.partecipantsId,
      isGroup: true,
      image: this.form.value.photo,
      createdAt: new Date(),
    };
    this.subscriptions.push(
      this.chatService
        .createGroup(group)
        .pipe(
          tap((res) => {
            this.closeCreateGroupModal();
            this.form.reset();
            this.chRef.detectChanges();
          }),
          catchError((err) => {
            console.error('Failed to create group', err);
            return [];
          })
        )
        .subscribe()
    );
  }

  // addUserToGroup(): void {
  //   const addUsersRequest: AddUserGroup = {
  //     conversationId: this.currentChat.id,
  //     userIds: this.form.value.partecipantsId,
  //   };
  //   this.subscriptions.push(
  //     this.chatService.addUserToGroup(addUsersRequest).pipe(
  //       tap((res: Chat) => {
  //         this.currentChat = res;
  //         this.groupPreview = this.currentChat.chatParticipants.map((participant: UserType) => participant!.photo);
  //         this.chRef.detectChanges();
  //       }),
  //       catchError((err) => {
  //         console.error('Failed to add user to group', err);
  //         return [];
  //       })
  //     ).subscribe()
  //   );
  // }

  updateGroup(): void {
    if (this.currentChat) {
      const editedGroup: Chat = {
        id: this.currentChat.id,
        name: this.editedForm.value.name,
        chatParticipants: this.editedForm.value.partecipantsId,
        isGroup: true,
        image: this.editedForm.value.photo,
        createdAt: this.currentChat.createdAt,
      };

      this.subscriptions.push(
        this.chatService.editGroup(editedGroup).pipe(
          tap((chat) => {
            this.currentChat = chat;
            this.closeEditGroupModal();
            this.chRef.detectChanges();
          }),
          catchError((error) => {
            console.error('Failed to update group', error);
            return [];
          })
        ).subscribe()
      );
    }
  }

  pushCollaborator(event: Event, collaborator: UserType): void {
    const checkbox = event.target as HTMLInputElement;
    if (checkbox.checked) {
      this.form.patchValue({
        partecipantsId: [...this.form.value.partecipantsId, collaborator?.id],
      });
    } else {
      this.form.patchValue({
        partecipantsId: this.form.value.partecipantsId.filter(
          (id: any) => id !== collaborator?.id
        ),
      });
    }
  }

  pushEditMember(event: Event, member: UserType): void {
    const checkbox = event.target as HTMLInputElement;
    if (checkbox.checked) {
      this.editedForm.patchValue({
        partecipantsId: this.editedForm.value.partecipantsId.filter(
          (id: any) => id !== member?.id
        ),
      });
      console.log(this.editedForm.value.partecipantsId);
    } else {
      this.editedForm.patchValue({
        partecipantsId: [...this.editedForm.value.partecipantsId, member?.id],
      });
      console.log(this.editedForm.value.partecipantsId);
    }
  }

  onFileSelected(event: any): void {
    const image: File = event.target.files[0];
    if (image.type.startsWith('image/')) {
      const reader = new FileReader();

      reader.onload = (e) => {
        const image64 = e.target?.result as string;
        this.imageSrc = image64;  // Mostra l'anteprima dell'immagine

        this.form.patchValue({ photo: image });  // Aggiungi il file al form
        this.chRef.detectChanges();
      };

      reader.readAsDataURL(image);
    }
  }

  onEditFormFileSelected(event: any): void {
    const image: File = event.target.files[0];
    if (image.type.startsWith('image/')) {
      const reader = new FileReader();
      reader.onload = (e) => {
        const image64 = e.target?.result as string;
        this.editedGroupImage = image64;
        this.editedForm.patchValue({ photo: image });
        this.chRef.detectChanges();
      };
      reader.readAsDataURL(image);
    }
  }

  get filteredGroup(): Chat[] {
    if (!this.searchTerm) {
      return this.groups;
    }
    return this.groups.filter((group) =>
      group.name?.toLowerCase().includes(this.searchTerm.toLowerCase())
    );
  }

  get filteredGroupChat(): string[] {
    if (this.groupPreview.length > 6) {
      this.groupLength = this.groupPreview.length - 6;
      return this.groupPreview.slice(0, 6);
    }
    return this.groupPreview;
  }

  async openCreateGroupModal(): Promise<any> {
    return await this.createGroupModal.open();
  }

  async closeCreateGroupModal(): Promise<any> {
    return await this.createGroupModal.close();
  }

  async openAddUserModal(): Promise<any> {
    return await this.addUserModal.open();
  }

  async closeAddUserModal(): Promise<any> {
    return await this.addUserModal.close();
  }

  async openEditGroupModal(): Promise<any> {
    return await this.editGroupModal.open();
  }

  async closeEditGroupModal(): Promise<any> {
    return await this.editGroupModal.close();
  }

  deleteGroup() {
    if (this.currentChat) {
      this.chatService.deleteChat(this.currentChat.id).subscribe();
    }
  }

  private subscribeToSelectedChat(): void {
    this.subscriptions.push(
      this.chatService.selectedChat$
        .pipe(
          tap((chat) => {
            if (chat) {
              this.currentChat = chat;
              this.initEditedForm(); // Reinitialize the form when currentChat changes
              this.chRef.detectChanges();
            }
          }),
          catchError((error) => {
            console.error('Failed to subscribe to selected chat', error);
            return [];
          })
        )
        .subscribe()
    );
  }
}
import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { Subscription, firstValueFrom } from 'rxjs';
import { ChatService } from 'src/app/services/Api/chat.service';
import { AuthService, UserType } from 'src/app/modules/auth';
import { ChatHistory } from 'src/app/modules/apps/chat/model/ChatHistory';
import { Chat } from 'src/app/modules/apps/chat/model/Chat';
import { ChatRequest } from 'src/app/modules/apps/chat/model/ChatRequest';
import { ChatResponse } from 'src/app/modules/apps/chat/model/ChatResponse';

@Component({
  selector: 'app-chat-inner',
  templateUrl: './chat-inner.component.html',
  styleUrls: ['./chat-inner.component.scss'],
})
export class ChatInnerComponent implements OnInit, OnDestroy {
  profile: UserType;
  message: string = '';
  messageHistory: ChatHistory;
  currentChat: Chat;
  private subscriptions: Subscription[] = [];
  private isNewChat: boolean = false;

  constructor(
    private chatService: ChatService,
    private authService: AuthService,
    private chRef: ChangeDetectorRef
  ) {}

  ngOnInit(): void {
    this.profile = this.authService.currentUserValue;
    this.subscriptions.push(
      this.chatService.selectedChat$.subscribe((chat) => this.onChatSelected(chat))
    );
  }

  ngOnDestroy(): void {
    this.subscriptions.forEach((sub) => sub.unsubscribe());
    if (this.currentChat) {
      this.chatService.unsubscribe(this.currentChat.id);
    }
  }

  private onChatSelected(chat: Chat | null): void {
    if (chat) {
      this.currentChat = chat;
      this.getMessageHistory();
      this.subscribeToMessages();
    }
  }

  async submitMessage() {
    const chatRequest = this.createChatRequest();

    this.message = '';

    if (chatRequest.conversationId === -1) {
      await this.handleNewChat(chatRequest);
    }

    try {
      this.chatService.produceMessage(chatRequest);
    } catch (error) {
      console.error('Failed to send message', error);
    }
  }

  private createChatRequest(): ChatRequest {
    return {
      conversationId: this.currentChat.id,
      senderId: this.profile!.id,
      message: this.message,
      isGroup: this.currentChat.isGroup,
      receiverId: this.getReceiverId(),
      name: '',
      participantIds: [this.profile!.id, this.currentChat.id],
    };
  }

  private getReceiverId(): number {
    return this.currentChat.isGroup
      ? -1
      : this.currentChat.chatParticipants.find(
          (participant) => participant?.id !== this.profile?.id
        )?.id || -1;
  }

  private async handleNewChat(chatRequest: ChatRequest) {
    this.isNewChat = true;
    const res = await firstValueFrom(this.chatService.createChat(chatRequest));
    this.chatService.selectChat(res);
    chatRequest.conversationId = res.id;
  }

  getMessageCssClass(message: ChatResponse): string {
    const isMe = this.isMe(message.senderId);
    return `p-5 rounded text-gray-900 fw-bold mw-lg-400px bg-light-${isMe ? 'primary' : 'info'} text-${isMe ? 'end' : 'start'}`;
  }

  getMessageHistory(): void {
    if (this.isNewChat) {
      this.isNewChat = false;
      return;
    }

    if (this.profile && this.currentChat && this.currentChat.id !== -1) {
      this.subscriptions.push(
        this.chatService.getChatHistory(this.currentChat.id).subscribe((res) => {
          this.messageHistory = res;
          this.chRef.detectChanges();
        })
      );
    } else {
      this.messageHistory = { chatId: this.currentChat.id, messages: [] };
    }
  }

  isMe(senderId: number): boolean {
    return senderId === this.profile?.id;
  }

  private subscribeToMessages(): void {
    this.chatService.consumeMessage(this.currentChat.id, (message) => {
      if (message.senderId === this.profile?.id) {
        this.updatePendingMessageStatus(message);
      } else {
        this.messageHistory.messages.push(message);
      }
      this.chRef.detectChanges();
    });
  }

  private updatePendingMessageStatus(message: ChatResponse): void {
    const pendingMessage = this.messageHistory.messages.find(
      (m) => m.chatId === this.currentChat.id && m.status === 'PENDING'
    );
    if (pendingMessage) {
      pendingMessage.status = message.status;
      console.log('Confirmed');
    }
  }

  getUser(senderId: number): UserType | undefined {
    return this.currentChat.chatParticipants.find(
      (participant) => participant?.id === senderId
    );
  }
}