Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
gokhoal
2026-06-11 01:30:43 +02:00
12 changed files with 394 additions and 24 deletions
+4
View File
@@ -28,4 +28,8 @@ export class discussionsService {
createPrivateDiscussion(username: string): Observable<Discussion> {
return this.http.post<Discussion>(`${this.apiUrl}/discussions/private`, { username });
}
createGroupDiscussion(groupName: string, usernames: string[]): Observable<Discussion> {
return this.http.post<Discussion>(`${this.apiUrl}/discussions/group`, { groupName, usernames });
}
}
@@ -108,3 +108,158 @@
box-shadow: 0 1px 5px rgba(180, 80, 80, 0.1);
}
}
.mode-switch {
margin-top: auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
backdrop-filter: blur(8px);
box-shadow: 0 2px 12px rgba(122, 46, 46, 0.08);
}
.switch-label {
color: #7a2e2e;
font-weight: 600;
font-size: 0.95rem;
}
/* SWITCH */
.switch {
position: relative;
width: 58px;
height: 32px;
}
.switch input {
display: none;
}
.slider {
position: absolute;
inset: 0;
cursor: pointer;
background: rgba(255, 255, 255, 0.6);
border-radius: 999px;
transition: all 0.25s ease;
box-shadow:
inset 0 1px 3px rgba(0,0,0,0.08),
0 2px 8px rgba(0,0,0,0.05);
}
.slider::before {
content: "";
position: absolute;
top: 4px;
left: 4px;
width: 24px;
height: 24px;
border-radius: 50%;
background: white;
transition: all 0.25s ease;
box-shadow:
0 2px 8px rgba(0,0,0,0.15);
}
.switch input:checked + .slider {
background: #bd5a5a;
}
.switch input:checked + .slider::before {
transform: translateX(26px);
}
.mode-switch:hover .slider {
transform: scale(1.03);
}
.add-member-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 12px 16px;
border: 2px dashed rgba(189, 90, 90, 0.35);
border-radius: 14px;
background: rgba(255, 255, 255, 0.55);
color: #bd5a5a;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
ion-icon {
font-size: 1.2rem;
}
&:hover {
background: rgba(255, 255, 255, 0.8);
border-color: #bd5a5a;
transform: translateY(-1px);
}
&:active {
transform: scale(0.98);
}
}
.remove-btn {
width: 26px;
height: 26px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
background: white;
color: #bd5a5a;
cursor: pointer;
box-shadow: 0 2px 8px rgba(180, 80, 80, 0.12);
transition: all 0.2s ease;
ion-icon {
font-size: 1.1rem;
}
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(180, 80, 80, 0.18);
}
&:active {
transform: scale(0.92);
}
}
@@ -11,13 +11,18 @@
<div class="modal-layout">
<div class="modal-header">
<h2 class="modal-title">Nouvelle conversation</h2>
<h2 class="modal-title">{{ isGroup() ? 'Nouveau groupe' : 'Nouvelle conversation' }}</h2>
<div class="header-actions">
<button class="modal-close-btn" (click)="closeNav()">
<ion-icon name="close-outline" />
</button>
</div>
</div>
<div class="modal-body">
<!-- Mode privé -->
<ng-container *ngIf="!isGroup()">
<div class="input-wrapper">
<ion-item class="custom-item">
<ion-input
@@ -29,12 +34,68 @@
</ion-item>
<p class="error-msg" *ngIf="errorMsg()">{{ errorMsg() }}</p>
</div>
<button class="submit-btn" (click)="startConversation()" [disabled]="isLoading()">
{{ isLoading() ? 'Création...' : 'Démarrer la conversation' }}
</button>
</ng-container>
<!-- Mode groupe -->
<ng-container *ngIf="isGroup()">
<div class="input-wrapper">
<ion-item class="custom-item">
<ion-input
[formControl]="groupName"
label="Nom du groupe"
labelPlacement="floating"
placeholder="ex: Les amis"
/>
</ion-item>
</div>
<div class="members-list">
<div class="member-row" *ngFor="let ctrl of groupMembers; let i = index">
<ion-item class="custom-item member-item">
<ion-input
[formControl]="ctrl"
label="Membre {{ i + 1 }}"
labelPlacement="floating"
placeholder="ex: jean_dupont"
/>
</ion-item>
<button class="remove-btn" (click)="removeMember(i)" *ngIf="groupMembers.length > 1">
<ion-icon name="close-outline" />
</button>
</div>
</div>
<button class="add-member-btn" (click)="addMember()" *ngIf="groupMembers.length < 10">
<ion-icon name="add-outline" />
Ajouter un membre
</button>
<p class="error-msg" *ngIf="errorMsg()">{{ errorMsg() }}</p>
<button class="submit-btn" (click)="startGroupConversation()" [disabled]="isLoading()">
{{ isLoading() ? 'Création...' : 'Créer le groupe' }}
</button>
</ng-container>
<div class="mode-switch">
<span class="switch-label">
{{ isGroup() ? 'Mode groupe' : 'Conversation privée' }}
</span>
<label class="switch">
<input
type="checkbox"
[checked]="isGroup()"
(change)="toggleMode()"
>
<span class="slider"></span>
</label>
</div>
</div>
</div>
</ng-template>
</ion-modal>
@@ -2,14 +2,14 @@ import { Component, inject, signal } from '@angular/core';
import { Router } from "@angular/router";
import { ReactiveFormsModule, FormControl } from "@angular/forms";
import { addIcons } from "ionicons";
import { closeOutline } from "ionicons/icons";
import { IonIcon, IonInput, IonItem, IonModal } from "@ionic/angular/standalone";
import { closeOutline, addOutline } from "ionicons/icons";
import { discussionsService } from "../../../core/chat/discussion.service";
import { CommonModule } from "@angular/common";
import {CommonModule} from "@angular/common";
@Component({
selector: 'app-menu-nav',
imports: [CommonModule, IonModal, IonItem, IonInput, IonIcon, ReactiveFormsModule],
imports: [IonModal, IonItem, IonInput, IonIcon, ReactiveFormsModule, CommonModule],
templateUrl: './menu-nav.component.html',
styleUrl: './menu-nav.component.css'
})
@@ -21,11 +21,17 @@ export class MenuNav {
isModalOpen = signal(false);
isLoading = signal(false);
errorMsg = signal<string | null>(null);
isGroup = signal(false);
// Mode privé
username = new FormControl('');
// Mode groupe
groupName = new FormControl('');
groupMembers: FormControl[] = [new FormControl('')];
constructor() {
addIcons({ closeOutline });
addIcons({ closeOutline, addOutline });
}
openNav() { this.isModalOpen.set(true); }
@@ -33,7 +39,25 @@ export class MenuNav {
closeNav() {
this.isModalOpen.set(false);
this.username.reset();
this.groupName.reset();
this.groupMembers = [new FormControl('')];
this.errorMsg.set(null);
this.isGroup.set(false);
}
toggleMode() {
this.isGroup.update(v => !v);
this.errorMsg.set(null);
}
addMember() {
if (this.groupMembers.length < 10) {
this.groupMembers.push(new FormControl(''));
}
}
removeMember(index: number) {
this.groupMembers.splice(index, 1);
}
startConversation() {
@@ -57,4 +81,30 @@ export class MenuNav {
}
});
}
startGroupConversation() {
const name = this.groupName.value?.trim();
const usernames = this.groupMembers
.map(c => c.value?.trim())
.filter(v => !!v);
if (!name || usernames.length === 0 || this.isLoading()) return;
this.isLoading.set(true);
this.errorMsg.set(null);
this.discussionService.createGroupDiscussion(name, usernames).subscribe({
next: (discussion) => {
this.isLoading.set(false);
this.closeNav();
this.router.navigate(['/main/messages', discussion.id]);
},
error: (err) => {
this.isLoading.set(false);
this.errorMsg.set(
err.status === 404 ? 'Un ou plusieurs utilisateurs introuvables.' : 'Une erreur est survenue.'
);
}
});
}
}
@@ -2,5 +2,5 @@
<div class="icon-wrapper">
<img width="50" height="50" src="https://img.icons8.com/ios/50/user-male-circle--v1.png" alt="user"/>
</div>
<span class="username">Nom User</span>
<span class="username">{{ name || 'Utilisateur' }}</span>
</button>
@@ -1,4 +1,5 @@
import { Component } from '@angular/core';
import {Component, inject, Input} from '@angular/core';
import {AuthService} from "../../../core/auth/auth.service";
@Component({
selector: 'app-messages-infouser',
@@ -7,5 +8,8 @@ import { Component } from '@angular/core';
styleUrl: './messages-infouser.component.css'
})
export class MessagesInfoUser {
@Input() name: string = '';
private authService = inject(AuthService);
user = this.authService.currentUser;
}
@@ -2,7 +2,7 @@
<div class="header">
<app-messages-menu/>
<app-messages-infouser/>
<app-messages-infouser [name]="discussionName"/>
</div>
<div class="messages">
@@ -24,11 +24,19 @@ export class MessagesMain implements OnInit {
currentDiscussionId!: string;
currentUserId!: number;
messages: Message[] = [];
discussionName: string = '';
async ngOnInit() {
this.currentDiscussionId = this.route.snapshot.paramMap.get('discussionId')!;
this.currentUserId = this.authService.getCurrentUserId();
this.discussionService.getDiscussions().subscribe({
next: (discussions) => {
const discussion = discussions.find(d => d.id === +this.currentDiscussionId);
this.discussionName = discussion?.name ?? '';
}
});
this.discussionService.getMessages(this.currentDiscussionId).subscribe({
next: (messages) => this.messages = messages,
error: (err) => console.error('Impossible de charger les messages', err)
@@ -39,6 +39,7 @@ model/knots-dto-user-update-user-description-dto.ts
model/knots-dto-user-update-user-password-dto.ts
model/knots-dto-user-update-user-profile-picture-dto.ts
model/knots-dto-user-update-username-dto.ts
model/knots-endpoints-discussion-create-group-discussion-request.ts
model/knots-endpoints-discussion-create-private-discussion-request.ts
model/models.ts
param.ts
@@ -25,6 +25,8 @@ import { KnotsDTODiscussionDeleteDiscussionDto } from '../model/knots-dto-discus
// @ts-ignore
import { KnotsDTOMessageGetMessageDetailsDto } from '../model/knots-dto-message-get-message-details-dto';
// @ts-ignore
import { KnotsEndpointsDiscussionCreateGroupDiscussionRequest } from '../model/knots-endpoints-discussion-create-group-discussion-request';
// @ts-ignore
import { KnotsEndpointsDiscussionCreatePrivateDiscussionRequest } from '../model/knots-endpoints-discussion-create-private-discussion-request';
// @ts-ignore
@@ -108,6 +110,74 @@ export class DiscussionsService extends BaseService {
);
}
/**
* @endpoint post /API/discussions/group
* @param knotsEndpointsDiscussionCreateGroupDiscussionRequest
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
* @param options additional options
*/
public createGroupDiscussionEndpoint(knotsEndpointsDiscussionCreateGroupDiscussionRequest: KnotsEndpointsDiscussionCreateGroupDiscussionRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<object>;
public createGroupDiscussionEndpoint(knotsEndpointsDiscussionCreateGroupDiscussionRequest: KnotsEndpointsDiscussionCreateGroupDiscussionRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<object>>;
public createGroupDiscussionEndpoint(knotsEndpointsDiscussionCreateGroupDiscussionRequest: KnotsEndpointsDiscussionCreateGroupDiscussionRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<object>>;
public createGroupDiscussionEndpoint(knotsEndpointsDiscussionCreateGroupDiscussionRequest: KnotsEndpointsDiscussionCreateGroupDiscussionRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<any> {
if (knotsEndpointsDiscussionCreateGroupDiscussionRequest === null || knotsEndpointsDiscussionCreateGroupDiscussionRequest === undefined) {
throw new Error('Required parameter knotsEndpointsDiscussionCreateGroupDiscussionRequest was null or undefined when calling createGroupDiscussionEndpoint.');
}
let localVarHeaders = this.defaultHeaders;
// authentication (JWTBearerAuth) required
localVarHeaders = this.configuration.addCredentialToHeaders('JWTBearerAuth', 'Authorization', localVarHeaders, 'Bearer ');
const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([
'application/json'
]);
if (localVarHttpHeaderAcceptSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
}
const localVarHttpContext: HttpContext = options?.context ?? new HttpContext();
const localVarTransferCache: boolean = options?.transferCache ?? true;
// to determine the Content-Type header
const consumes: string[] = [
'application/json'
];
const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes);
if (httpContentTypeSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected);
}
let responseType_: 'text' | 'json' | 'blob' = 'json';
if (localVarHttpHeaderAcceptSelected) {
if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
responseType_ = 'text';
} else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) {
responseType_ = 'json';
} else {
responseType_ = 'blob';
}
}
let localVarPath = `/API/discussions/group`;
const { basePath, withCredentials } = this.configuration;
return this.httpClient.request<object>('post', `${basePath}${localVarPath}`,
{
context: localVarHttpContext,
body: knotsEndpointsDiscussionCreateGroupDiscussionRequest,
responseType: <any>responseType_,
...(withCredentials ? { withCredentials } : {}),
headers: localVarHeaders,
observe: observe,
...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}),
reportProgress: reportProgress
}
);
}
/**
* @endpoint post /API/discussions/private
* @param knotsEndpointsDiscussionCreatePrivateDiscussionRequest
@@ -0,0 +1,16 @@
/**
* Knots
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface KnotsEndpointsDiscussionCreateGroupDiscussionRequest {
groupName?: string;
usernames?: Array<string>;
}
+1
View File
@@ -23,4 +23,5 @@ export * from './knots-dto-user-update-user-description-dto';
export * from './knots-dto-user-update-user-password-dto';
export * from './knots-dto-user-update-user-profile-picture-dto';
export * from './knots-dto-user-update-username-dto';
export * from './knots-endpoints-discussion-create-group-discussion-request';
export * from './knots-endpoints-discussion-create-private-discussion-request';