first commit
This commit is contained in:
commit
af661cf543
311 changed files with 31070 additions and 0 deletions
16
.editorconfig
Normal file
16
.editorconfig
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Firebase
|
||||||
|
.firebase
|
||||||
|
*-debug.log
|
||||||
|
.runtimeconfig.json
|
||||||
|
/src/app/environments/config.ts
|
||||||
4
.vscode/extensions.json
vendored
Normal file
4
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||||
|
"recommendations": ["angular.ng-template"]
|
||||||
|
}
|
||||||
20
.vscode/launch.json
vendored
Normal file
20
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "ng serve",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "npm: start",
|
||||||
|
"url": "http://localhost:4200/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ng test",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "npm: test",
|
||||||
|
"url": "http://localhost:9876/debug.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
42
.vscode/tasks.json
vendored
Normal file
42
.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "start",
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": {
|
||||||
|
"owner": "typescript",
|
||||||
|
"pattern": "$tsc",
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": {
|
||||||
|
"regexp": "(.*?)"
|
||||||
|
},
|
||||||
|
"endsPattern": {
|
||||||
|
"regexp": "bundle generation complete"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "test",
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": {
|
||||||
|
"owner": "typescript",
|
||||||
|
"pattern": "$tsc",
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": {
|
||||||
|
"regexp": "(.*?)"
|
||||||
|
},
|
||||||
|
"endsPattern": {
|
||||||
|
"regexp": "bundle generation complete"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
27
README.md
Normal file
27
README.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Dabubble
|
||||||
|
|
||||||
|
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.2.1.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||||
|
|
||||||
|
## Further help
|
||||||
|
|
||||||
|
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
||||||
92
angular.json
Normal file
92
angular.json
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"dabubble": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/dabubble",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"polyfills": ["zone.js"],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": ["src/favicon.ico", "src/assets"],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss",
|
||||||
|
"./node_modules/@ctrl/ngx-emoji-mart/picker.css"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "2mb",
|
||||||
|
"maximumError": "4mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "16kb",
|
||||||
|
"maximumError": "32kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "dabubble:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "dabubble:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "dabubble:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"polyfills": ["zone.js", "zone.js/testing"],
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": ["src/favicon.ico", "src/assets"],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss",
|
||||||
|
"./node_modules/@ctrl/ngx-emoji-mart/picker.css"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
firebase.json
Normal file
1
firebase.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
13515
package-lock.json
generated
Normal file
13515
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
45
package.json
Normal file
45
package.json
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"name": "dabubble",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^17.2.0",
|
||||||
|
"@angular/cdk": "^17.3.1",
|
||||||
|
"@angular/common": "^17.2.0",
|
||||||
|
"@angular/compiler": "^17.2.0",
|
||||||
|
"@angular/core": "^17.2.0",
|
||||||
|
"@angular/fire": "^17.0.1",
|
||||||
|
"@angular/forms": "^17.2.0",
|
||||||
|
"@angular/platform-browser": "^17.2.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^17.2.0",
|
||||||
|
"@angular/router": "^17.2.0",
|
||||||
|
"@ctrl/ngx-emoji-mart": "^9.2.0",
|
||||||
|
"@ngx-translate/core": "^15.0.0",
|
||||||
|
"@ngx-translate/http-loader": "^8.0.0",
|
||||||
|
"ng2-pdf-viewer": "^10.0.0",
|
||||||
|
"ngx-extended-pdf-viewer": "^19.6.6",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.14.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^17.2.1",
|
||||||
|
"@angular/cli": "^17.2.1",
|
||||||
|
"@angular/compiler-cli": "^17.2.0",
|
||||||
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"jasmine-core": "~5.1.0",
|
||||||
|
"karma": "~6.4.0",
|
||||||
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
"karma-coverage": "~2.2.0",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"typescript": "~5.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/app/app.component.html
Normal file
3
src/app/app.component.html
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
|
||||||
0
src/app/app.component.scss
Normal file
0
src/app/app.component.scss
Normal file
29
src/app/app.component.spec.ts
Normal file
29
src/app/app.component.spec.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AppComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should have the 'dabubble' title`, () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.componentInstance;
|
||||||
|
expect(app.title).toEqual('dabubble');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render title', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, dabubble');
|
||||||
|
});
|
||||||
|
});
|
||||||
17
src/app/app.component.ts
Normal file
17
src/app/app.component.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
import { UserComponent } from './shared/components/user/user.component';
|
||||||
|
import { EditUserDetailsComponent } from './shared/components/header/edit-user/edit-user-details/edit-user-details.component';
|
||||||
|
import { ToggleBooleanService } from './service/toggle-boolean.service';
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
standalone: true,
|
||||||
|
imports: [RouterOutlet, UserComponent, EditUserDetailsComponent],
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrl: './app.component.scss',
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
title = 'dabubble';
|
||||||
|
|
||||||
|
constructor(public toggleAllBooleans:ToggleBooleanService){}
|
||||||
|
}
|
||||||
38
src/app/app.config.ts
Normal file
38
src/app/app.config.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { HttpClientModule, HttpClient } from '@angular/common/http';
|
||||||
|
import { routes } from './app.routes';
|
||||||
|
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
|
||||||
|
import { getFirestore, provideFirestore } from '@angular/fire/firestore';
|
||||||
|
import { provideStorage, getStorage } from '@angular/fire/storage';
|
||||||
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
|
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
||||||
|
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||||
|
import { firebaseConfig } from './environments/config';
|
||||||
|
|
||||||
|
export function HttpLoaderFactory(httpClient: HttpClient) {
|
||||||
|
return new TranslateHttpLoader(httpClient, './assets/i18n/', '.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
const translationConfig = {
|
||||||
|
defaultLanguage: 'de',
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useFactory: HttpLoaderFactory,
|
||||||
|
deps: [HttpClient],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideRouter(routes),
|
||||||
|
importProvidersFrom(
|
||||||
|
HttpClientModule,
|
||||||
|
provideFirebaseApp(() => initializeApp(firebaseConfig)),
|
||||||
|
provideFirestore(() => getFirestore()),
|
||||||
|
provideStorage(() => getStorage()),
|
||||||
|
TranslateModule.forRoot(translationConfig)
|
||||||
|
),
|
||||||
|
provideAnimationsAsync(),
|
||||||
|
],
|
||||||
|
};
|
||||||
22
src/app/app.routes.ts
Normal file
22
src/app/app.routes.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Routes } from '@angular/router';
|
||||||
|
import { MainComponent } from './components/main/main.component';
|
||||||
|
import { RegisterComponent } from './components/main-login/register/register.component';
|
||||||
|
import { ChooseAvatarComponent } from './components/main-login/choose-avatar/choose-avatar.component';
|
||||||
|
import { PasswordForgetComponent } from './components/main-login/password-forget/password-forget.component';
|
||||||
|
import { PasswordResetComponent } from './components/main-login/password-reset/password-reset.component';
|
||||||
|
import { LoginComponent } from './components/main-login/login/login.component';
|
||||||
|
import { ImprintComponent } from './shared/components/imprint/imprint.component';
|
||||||
|
import { PrivacyPolicyComponent } from './shared/components/privacy-policy/privacy-policy.component';
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{ path: '', component: MainComponent },
|
||||||
|
{ path: 'login', component: LoginComponent },
|
||||||
|
{ path: 'register', component: RegisterComponent },
|
||||||
|
{ path: 'avatar', component: ChooseAvatarComponent },
|
||||||
|
{ path: 'passwordForget', component: PasswordForgetComponent },
|
||||||
|
{ path: 'passwordReset', component: PasswordResetComponent },
|
||||||
|
{ path: 'main', component: MainComponent },
|
||||||
|
{ path: 'main/:id', component: MainComponent },
|
||||||
|
{ path: 'imprint', component: ImprintComponent },
|
||||||
|
{ path: 'privacy-policy', component: PrivacyPolicyComponent },
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
<div class="menu" (click)="closeMenu()">
|
||||||
|
<div class="whiteBox" (click)="preventCloseWhiteBox($event)">
|
||||||
|
@for (channel of getChannelName(currentChannel); track channel) {
|
||||||
|
<div class="headerBox">
|
||||||
|
<div class="headline">
|
||||||
|
<img src="./assets/img/hashtag.svg" alt="" />
|
||||||
|
<p>{{ channel.name }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="closeBtn" (click)="closeMenu()">
|
||||||
|
<img src="./assets/img/closeIcon.svg" alt="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="contentChannel">
|
||||||
|
@for (channel of getChannelName(currentChannel); track channel) {
|
||||||
|
<div class="contentHeadline">
|
||||||
|
<p>{{ "channel-informations.header" | translate }}</p>
|
||||||
|
@if (!openEditNameInput) {
|
||||||
|
<!--check if user is creator-->
|
||||||
|
@if(checkCreator(currentChannel)){
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'pencil.svg'"
|
||||||
|
[imgSize]="'18px'"
|
||||||
|
[btnSize]="'32px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
(click)="editChannelName($event)"
|
||||||
|
></app-small-btn>
|
||||||
|
} @else {
|
||||||
|
<img class="disableEdit" src="./assets/img/pencil.svg" alt="" />
|
||||||
|
} } @else {
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'save.svg'"
|
||||||
|
[imgSize]="'18px'"
|
||||||
|
[btnSize]="'32px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
(click)="saveEditChannelName($event)"
|
||||||
|
></app-small-btn>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (!openEditNameInput) {
|
||||||
|
<div class="channelName">
|
||||||
|
<img src="./assets/img/hashtag.svg" alt="" />
|
||||||
|
<p>{{ channel.name }}</p>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="channelName">
|
||||||
|
<img src="./assets/img/hashtag.svg" alt="" />
|
||||||
|
<input type="text" [(ngModel)]="nameValue" class="inputName" />
|
||||||
|
</div>
|
||||||
|
} }
|
||||||
|
</div>
|
||||||
|
<div class="contentChannel">
|
||||||
|
@for (channel of getChannelName(currentChannel); track channel) {
|
||||||
|
|
||||||
|
<div class="contentHeadline">
|
||||||
|
<p>{{ "channel-informations.channelInfo" | translate }}</p>
|
||||||
|
@if (!openEditNameDescription) {
|
||||||
|
<!--check if user is creator-->
|
||||||
|
@if(checkCreator(currentChannel)){
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'pencil.svg'"
|
||||||
|
[imgSize]="'18px'"
|
||||||
|
[btnSize]="'32px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
(click)="editChannelDescription($event)"
|
||||||
|
></app-small-btn>
|
||||||
|
} @else {
|
||||||
|
<img class="disableEdit" src="./assets/img/pencil.svg" alt="" />
|
||||||
|
} } @else {
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'save.svg'"
|
||||||
|
[imgSize]="'18px'"
|
||||||
|
[btnSize]="'32px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
(click)="saveEditChannelDescription($event)"
|
||||||
|
></app-small-btn>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (!openEditNameDescription) {
|
||||||
|
<div class="channelDescription">
|
||||||
|
@if (channel.description !== "") {
|
||||||
|
<p>{{ channel.description }}</p>
|
||||||
|
} @else {
|
||||||
|
<p>{{ "channel-informations.channelInfo2" | translate }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="channelName">
|
||||||
|
<textarea
|
||||||
|
cols="30"
|
||||||
|
rows="10"
|
||||||
|
[(ngModel)]="descriptionValue"
|
||||||
|
class="textareaDescription"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="line"></div>
|
||||||
|
<div class="contentHeadline">
|
||||||
|
<p>{{ "channel-informations.channelMadeBy" | translate }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="channelCreator">
|
||||||
|
@if(channel.creator == "Admin") {
|
||||||
|
<p class="creatorName">Admin</p>
|
||||||
|
} @else { @for (user of getChatUsers(channel.creator); track user) {
|
||||||
|
<p class="creatorName">{{ user.firstName }} {{ user.lastName }}</p>
|
||||||
|
} } @if(!checkCreator(currentChannel)){
|
||||||
|
<p class="warningPTag">
|
||||||
|
{{ "channel-informations.channelErrorText" | translate }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="contentChannelMembers"
|
||||||
|
[ngStyle]="{
|
||||||
|
display: viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? 'none' : ''
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="contentHeadline">
|
||||||
|
<p>{{ "channel-informations.members" | translate }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="allUsers">
|
||||||
|
@for(allMembers of getAllChannelMembers(currentChannel)[0].addedUser;
|
||||||
|
track allMembers) { @for (user of getChatUsers(allMembers); track user)
|
||||||
|
{
|
||||||
|
<div class="user" (click)="openUserWindow(user)">
|
||||||
|
<div class="imgBox">
|
||||||
|
<img src="{{ user.avatar }}" class="userImg" />
|
||||||
|
<img
|
||||||
|
src="./assets/img/{{
|
||||||
|
user.status ? 'onlineRing.svg' : 'offlineRing.svg'
|
||||||
|
}}"
|
||||||
|
class="onlineIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{{ user.firstName }} {{ user.lastName }} @if (user.id ==
|
||||||
|
userService.getCurrentUserId()) { (You) }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-open-send-prv-message-window
|
||||||
|
[ngClass]="{
|
||||||
|
dBlock: openUserWindowBoolean,
|
||||||
|
dNone: !openUserWindowBoolean
|
||||||
|
}"
|
||||||
|
[user]="user"
|
||||||
|
[openUserWindowBoolean]="openUserWindowBoolean"
|
||||||
|
(closeUserWondow)="changeOpenUserWindowBoolean($event)"
|
||||||
|
></app-open-send-prv-message-window>
|
||||||
|
<div class="leaveChannel">
|
||||||
|
<button
|
||||||
|
[disabled]="openEditNameInput || openEditNameDescription"
|
||||||
|
(click)="leaveChannel(currentChannel, $event)"
|
||||||
|
>
|
||||||
|
{{ "channel-informations.leave" | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,317 @@
|
||||||
|
@import "./../../../../styles.scss";
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 5;
|
||||||
|
.whiteBox {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 24px;
|
||||||
|
width: 460px;
|
||||||
|
border-top-right-radius: 40px;
|
||||||
|
border-bottom-left-radius: 40px;
|
||||||
|
border-bottom-right-radius: 40px;
|
||||||
|
position: absolute;
|
||||||
|
top: 188px;
|
||||||
|
left: 460px;
|
||||||
|
height: fit-content;
|
||||||
|
overflow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerBox {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
max-width: fit-content;
|
||||||
|
img {
|
||||||
|
&:first-child {
|
||||||
|
width: 24px;
|
||||||
|
height: 25px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
width: 14px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dieableEdit {
|
||||||
|
color: rgba(0, 0, 0, 0.5) !important;
|
||||||
|
cursor: context-menu !important;
|
||||||
|
&:hover {
|
||||||
|
font-weight: 400 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeBtn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 37px;
|
||||||
|
height: 37px;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: #edeefe;
|
||||||
|
border-radius: 100%;
|
||||||
|
img {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentChannel {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 40px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
p {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.contentHeadline {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
span {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #545af1;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelName {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 18px;
|
||||||
|
img {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.inputName {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #888dec;
|
||||||
|
outline: none;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
.textareaDescription {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
max-height: 104px;
|
||||||
|
border-radius: 40px;
|
||||||
|
border: 1px solid #888dec;
|
||||||
|
resize: none;
|
||||||
|
font-size: medium;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelDescription {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
|
padding-top: 18px;
|
||||||
|
p {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelCreator {
|
||||||
|
margin-top: 24px;
|
||||||
|
.creatorName {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 200;
|
||||||
|
color: #535af1;
|
||||||
|
}
|
||||||
|
.warningPTag {
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
margin: 24px 6px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaveChannel {
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaveChannel button {
|
||||||
|
padding: 12px 25px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #fff;
|
||||||
|
border: 0px;
|
||||||
|
background-color: #444df2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaveChannel button:disabled {
|
||||||
|
background-color: #999;
|
||||||
|
cursor: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaveChannel button:hover:not(:disabled) {
|
||||||
|
filter: opacity(70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.disableEdit {
|
||||||
|
width: 18px;
|
||||||
|
height: auto;
|
||||||
|
filter: brightness(0) saturate(100%) invert(59%) sepia(0%) saturate(0%)
|
||||||
|
hue-rotate(156deg) brightness(93%) contrast(84%);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MEMBERS
|
||||||
|
|
||||||
|
.contentChannelMembers {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 40px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
p {
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.contentHeadline {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.allUsers {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 6px;
|
||||||
|
padding: 6px;
|
||||||
|
max-width: fit-content;
|
||||||
|
cursor: pointer;
|
||||||
|
.imgBox {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
.userImg {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.onlineIcon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: -12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 0 12px 0 18px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: #edeefe;
|
||||||
|
border-radius: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------- RESPONSIVE -------------*/
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD) {
|
||||||
|
.menu {
|
||||||
|
.whiteBox {
|
||||||
|
top: 188px;
|
||||||
|
left: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE) {
|
||||||
|
.header {
|
||||||
|
height: 36px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
p {
|
||||||
|
font-size: 20px;
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
&:first-child {
|
||||||
|
width: 18px;
|
||||||
|
height: 19px;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
background-color: unset;
|
||||||
|
.whiteBox {
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 0;
|
||||||
|
width: calc(100% - 24px);
|
||||||
|
height: calc(100% - 24px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ChannelInformationsComponent } from './channel-informations.component';
|
||||||
|
|
||||||
|
describe('ChannelInformationsComponent', () => {
|
||||||
|
let component: ChannelInformationsComponent;
|
||||||
|
let fixture: ComponentFixture<ChannelInformationsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ChannelInformationsComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ChannelInformationsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { Channel } from '../../../interface/channel.interface';
|
||||||
|
import { ChannleService } from '../../../service/channle.service';
|
||||||
|
import { UserService } from '../../../service/user.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { User } from '../../../interface/user.interface';
|
||||||
|
import { SharedService } from '../../../service/shared.service';
|
||||||
|
import { SmallBtnComponent } from '../../../shared/components/small-btn/small-btn.component';
|
||||||
|
import { OpenSendPrvMessageWindowComponent } from '../show-channel-member/open-send-prv-message-window/open-send-prv-message-window.component';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-channel-informations',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
SmallBtnComponent,
|
||||||
|
OpenSendPrvMessageWindowComponent,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
templateUrl: './channel-informations.component.html',
|
||||||
|
styleUrl: './channel-informations.component.scss',
|
||||||
|
})
|
||||||
|
export class ChannelInformationsComponent {
|
||||||
|
@Input() currentChannel: string = '';
|
||||||
|
@Input() viewWidth: number = 0;
|
||||||
|
@Output() closeEditEmitter: EventEmitter<boolean> =
|
||||||
|
new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
openEditNameInput: boolean = false;
|
||||||
|
openEditNameDescription: boolean = false;
|
||||||
|
nameValue: string = '';
|
||||||
|
descriptionValue: string = '';
|
||||||
|
getCurrentChannel: Channel[] = [];
|
||||||
|
openUserWindowBoolean: boolean = false;
|
||||||
|
user: User[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: Router,
|
||||||
|
public channelService: ChannleService,
|
||||||
|
public userService: UserService,
|
||||||
|
private sharedService: SharedService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
RESPONSIVE_THRESHOLD_MOBILE = this.sharedService.RESPONSIVE_THRESHOLD_MOBILE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the menu by emitting a signal to close editing.
|
||||||
|
*/
|
||||||
|
showMenu() {
|
||||||
|
this.closeEditEmitter.emit(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the menu and resets various states.
|
||||||
|
*/
|
||||||
|
closeMenu() {
|
||||||
|
this.closeEditEmitter.emit(false);
|
||||||
|
this.openEditNameDescription = false;
|
||||||
|
this.openEditNameInput = false;
|
||||||
|
this.descriptionValue = '';
|
||||||
|
this.nameValue = '';
|
||||||
|
this.getCurrentChannel = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents the propagation of an event.
|
||||||
|
* @param {Event} event - The event to stop propagation.
|
||||||
|
*/
|
||||||
|
preventCloseWhiteBox(event: Event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the channel name based on its ID.
|
||||||
|
* @param {string} chatId - The ID of the channel.
|
||||||
|
* @returns {Channel[]} - The filtered channels.
|
||||||
|
*/
|
||||||
|
getChannelName(chatId: string) {
|
||||||
|
const filteredTasks = this.channelService.allChannels.filter(
|
||||||
|
(channel) => channel.id == chatId
|
||||||
|
);
|
||||||
|
this.getCurrentChannel = filteredTasks;
|
||||||
|
return filteredTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all channel members based on channel ID.
|
||||||
|
* @param {string} channelId - The ID of the channel.
|
||||||
|
* @returns {User[]} - The filtered users.
|
||||||
|
*/
|
||||||
|
getAllChannelMembers(channelId: string) {
|
||||||
|
return this.channelService.allChannels.filter(
|
||||||
|
(channel) => channel.id === channelId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves chat users based on user ID.
|
||||||
|
* @param {string} userId - The ID of the user.
|
||||||
|
* @returns {User[]} - The filtered users.
|
||||||
|
*/
|
||||||
|
getChatUsers(userId: string) {
|
||||||
|
return this.userService.allUsers.filter((user) => user.id === userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves channel members based on channel ID.
|
||||||
|
* @param {string} chatId - The ID of the channel.
|
||||||
|
* @returns {User[]} - The filtered users.
|
||||||
|
*/
|
||||||
|
getChannelMembers(chatId: string) {
|
||||||
|
const filteredTasks = this.userService.allUsers.filter(
|
||||||
|
(user) => user.id == chatId
|
||||||
|
);
|
||||||
|
return filteredTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current user is the creator of the channel.
|
||||||
|
* @param {string} currentChannel - The ID of the current channel.
|
||||||
|
* @returns {boolean} - True if the user is the creator, false otherwise.
|
||||||
|
*/
|
||||||
|
checkCreator(currentChannel: string) {
|
||||||
|
const getChannel = this.channelService.allChannels.filter(
|
||||||
|
(channel) => channel.id == currentChannel
|
||||||
|
);
|
||||||
|
if (getChannel[0].creator === this.userService.getCurrentUserId()) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a window displaying information about a user.
|
||||||
|
* @param {User} user - The user to display information about.
|
||||||
|
*/
|
||||||
|
openUserWindow(user: User) {
|
||||||
|
this.user = [user];
|
||||||
|
this.openUserWindowBoolean = !this.openUserWindowBoolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the state of the user window.
|
||||||
|
* @param {boolean} value - The new state of the user window.
|
||||||
|
*/
|
||||||
|
changeOpenUserWindowBoolean(value: boolean) {
|
||||||
|
this.openUserWindowBoolean = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates editing the channel name.
|
||||||
|
* @param {Event} event - The event that triggered the editing.
|
||||||
|
*/
|
||||||
|
editChannelName(event: Event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.openEditNameInput = true;
|
||||||
|
this.nameValue = this.getCurrentChannel[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the edited channel name.
|
||||||
|
* @param {Event} event - The event that triggered the save.
|
||||||
|
*/
|
||||||
|
saveEditChannelName(event: Event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.openEditNameInput = false;
|
||||||
|
this.channelService.saveAddedNameOrDescription(
|
||||||
|
'channels',
|
||||||
|
this.currentChannel!,
|
||||||
|
'name',
|
||||||
|
this.nameValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates editing the channel description.
|
||||||
|
* @param {Event} event - The event that triggered the editing.
|
||||||
|
*/
|
||||||
|
editChannelDescription(event: Event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.openEditNameDescription = true;
|
||||||
|
this.descriptionValue = this.getCurrentChannel[0].description || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the edited channel description.
|
||||||
|
* @param {Event} event - The event that triggered the save.
|
||||||
|
*/
|
||||||
|
saveEditChannelDescription(event: Event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.openEditNameDescription = false;
|
||||||
|
this.channelService.saveAddedNameOrDescription(
|
||||||
|
'channels',
|
||||||
|
this.currentChannel!,
|
||||||
|
'description',
|
||||||
|
this.descriptionValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the user to leave the channel.
|
||||||
|
* @param {string} currentChannel - The ID of the current channel.
|
||||||
|
* @param {Event} event - The event that triggered the action.
|
||||||
|
*/
|
||||||
|
leaveChannel(currentChannel: string, event: Event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const getLogedInUser: string = this.userService.getCurrentUserId();
|
||||||
|
const getChannel = this.channelService.allChannels.filter(
|
||||||
|
(channel) => channel.id == currentChannel
|
||||||
|
);
|
||||||
|
if (getChannel) {
|
||||||
|
const userIndex = getChannel[0].addedUser.indexOf(getLogedInUser);
|
||||||
|
if (userIndex !== -1) {
|
||||||
|
getChannel[0].addedUser.splice(userIndex, 1);
|
||||||
|
const userArray = getChannel[0].addedUser;
|
||||||
|
this.channelService.addNewMemberToChannel(
|
||||||
|
'channels',
|
||||||
|
currentChannel,
|
||||||
|
userArray,
|
||||||
|
'leaveChannel'
|
||||||
|
);
|
||||||
|
this.closeEditEmitter.emit(false);
|
||||||
|
this.route.navigateByUrl(`main`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.closeMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<section
|
||||||
|
[ngClass]="{
|
||||||
|
privatChatHeight: isPrivatChannel,
|
||||||
|
searchChatHeight: isSearchChannel
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
@if (!hideContentWindow) {
|
||||||
|
<div class="content" #messageBody>
|
||||||
|
<app-info [currentChannel]="currentChannel"></app-info>
|
||||||
|
@for (chat of getChatChannel(currentChannel); track chat; let i = $index) {
|
||||||
|
@for (user of getChatUsers(chat.userId); track user) {
|
||||||
|
@if(!isPublishedToday(chat.publishedTimestamp)) {
|
||||||
|
<div class="line">
|
||||||
|
<span class="lines"></span>
|
||||||
|
<p>{{ convertTimestampDate(chat.publishedTimestamp) }}</p>
|
||||||
|
<span class="lines"></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="singleChat">
|
||||||
|
<app-single-chat
|
||||||
|
[user]="user"
|
||||||
|
[chat]="chat"
|
||||||
|
[index]="i"
|
||||||
|
[currentChat]="chat.id"
|
||||||
|
[showAnswer]="true"
|
||||||
|
[openOnSecondaryChat]="false"
|
||||||
|
[isPrivatChannel]="isPrivatChannel"
|
||||||
|
[viewWidth]="viewWidth"
|
||||||
|
></app-single-chat>
|
||||||
|
</div>
|
||||||
|
} }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<app-chat-msg-box
|
||||||
|
[currentChannel]="currentChannel"
|
||||||
|
(newMsgEmitter)="editMsgEmitter($event)"
|
||||||
|
[target]="'chats'"
|
||||||
|
></app-chat-msg-box>
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
@import "../../../../styles.scss";
|
||||||
|
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: end;
|
||||||
|
height: calc(100vh - 210px);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privatChatHeight {
|
||||||
|
height: calc(100vh - 230px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchChatHeight {
|
||||||
|
margin-top: 64px;
|
||||||
|
height: calc(100vh - 274px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mirror {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin: 26px 0;
|
||||||
|
.lines {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #adb0d9;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
border: 1px solid #adb0d9;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------- responsive ---------------
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE) {
|
||||||
|
section {
|
||||||
|
height: calc(100vh - 144px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.privatChatHeight {
|
||||||
|
height: calc(100vh - 158px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchChatHeight {
|
||||||
|
margin-top: 88px;
|
||||||
|
height: calc(100vh - 188px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 380px) {
|
||||||
|
section {
|
||||||
|
height: calc(100vh - 144px);
|
||||||
|
}
|
||||||
|
.privatChatHeight {
|
||||||
|
height: calc(100vh - 158px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ChatContentComponent } from './chat-content.component';
|
||||||
|
|
||||||
|
describe('ChatContentComponent', () => {
|
||||||
|
let component: ChatContentComponent;
|
||||||
|
let fixture: ComponentFixture<ChatContentComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ChatContentComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ChatContentComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
import {
|
||||||
|
AfterViewChecked,
|
||||||
|
AfterViewInit,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
Input,
|
||||||
|
Renderer2,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MainChatComponent } from '../main-chat.component';
|
||||||
|
import { ChatService } from '../../../service/chat.service';
|
||||||
|
import { UserService } from '../../../service/user.service';
|
||||||
|
import { SingleChatComponent } from '../single-chat/single-chat.component';
|
||||||
|
import { ChatMsgBoxComponent } from '../chat-msg-box/chat-msg-box.component';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { DownloadFilesService } from '../../../service/download-files.service';
|
||||||
|
import { ChannleService } from '../../../service/channle.service';
|
||||||
|
import { InfoComponent } from '../info/info.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chat-content',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
MainChatComponent,
|
||||||
|
SingleChatComponent,
|
||||||
|
ChatMsgBoxComponent,
|
||||||
|
CommonModule,
|
||||||
|
InfoComponent,
|
||||||
|
],
|
||||||
|
templateUrl: './chat-content.component.html',
|
||||||
|
styleUrl: './chat-content.component.scss',
|
||||||
|
})
|
||||||
|
export class ChatContentComponent implements AfterViewInit, AfterViewChecked {
|
||||||
|
@Input() currentChannel: string = '';
|
||||||
|
@Input() isPrivatChannel: boolean = false;
|
||||||
|
@Input() isSearchChannel: boolean = false;
|
||||||
|
@Input() hideContentWindow: boolean = false;
|
||||||
|
@Input() viewWidth: number = 0;
|
||||||
|
@Input() getChatChannel!: (currentChannel: string) => any;
|
||||||
|
@Input() getChatUsers!: (currentChannel: string) => any;
|
||||||
|
@ViewChild('messageBody') messageBody: ElementRef | undefined;
|
||||||
|
filesLoaded: boolean = false;
|
||||||
|
isNewMessage: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private chatService: ChatService,
|
||||||
|
private userService: UserService,
|
||||||
|
public channelService: ChannleService,
|
||||||
|
private downloadFilesService: DownloadFilesService,
|
||||||
|
private renderer: Renderer2
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.scrollToBottom();
|
||||||
|
this.checkIfLoadedFirebaseFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewChecked() {
|
||||||
|
if (this.filesLoaded) {
|
||||||
|
this.scrollToBottom();
|
||||||
|
this.filesLoaded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the state of isNewMessage and optionally scrolls to the bottom.
|
||||||
|
* @param {boolean} variable - The new value for isNewMessage.
|
||||||
|
*/
|
||||||
|
editMsgEmitter(variable: boolean) {
|
||||||
|
this.isNewMessage = variable;
|
||||||
|
if (this.isNewMessage) {
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether Firebase files have been loaded from the server
|
||||||
|
*/
|
||||||
|
checkIfLoadedFirebaseFiles() {
|
||||||
|
this.downloadFilesService.downloadedFiles.subscribe((files) => {
|
||||||
|
if (files.length > 0) {
|
||||||
|
this.filesLoaded = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls to the bottom of the message body element.
|
||||||
|
*/
|
||||||
|
scrollToBottom(): void {
|
||||||
|
if (this.messageBody) {
|
||||||
|
const element = this.messageBody.nativeElement;
|
||||||
|
this.renderer.setProperty(
|
||||||
|
element,
|
||||||
|
'scrollTop',
|
||||||
|
element.scrollHeight - element.clientHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether multiple chats have been posted for the date
|
||||||
|
* @param {number} timestamp - The timestamp to be checked.
|
||||||
|
* @returns {boolean} - Returns true or false.
|
||||||
|
*/
|
||||||
|
isPublishedToday(timestamp: number) {
|
||||||
|
return this.getAllChatsOnTheDate(this.currentChannel, timestamp).includes(
|
||||||
|
timestamp
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves timestamps of all chats posted on a specific date in a given channel.
|
||||||
|
* @param {string} currentChannel - The current channel ID.
|
||||||
|
* @param {number} timestamp - The timestamp representing the date.
|
||||||
|
* @returns {number[]} An array of timestamps of messages.
|
||||||
|
*/
|
||||||
|
getAllChatsOnTheDate(currentChannel: string, timestamp: number) {
|
||||||
|
const todayDate = new Date(timestamp * 1000);
|
||||||
|
const todayTimestamps = this.chatService.allChats
|
||||||
|
.filter((chat) => chat.channelId === currentChannel)
|
||||||
|
.filter((chat) => {
|
||||||
|
const chatDate = new Date(chat.publishedTimestamp * 1000);
|
||||||
|
return (
|
||||||
|
chatDate.getFullYear() === todayDate.getFullYear() &&
|
||||||
|
chatDate.getMonth() === todayDate.getMonth() &&
|
||||||
|
chatDate.getDate() === todayDate.getDate()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((chat) => chat.publishedTimestamp);
|
||||||
|
|
||||||
|
todayTimestamps.shift();
|
||||||
|
return todayTimestamps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a timestamp to a formatted date string.
|
||||||
|
* @param {number} timestamp - The timestamp to convert.
|
||||||
|
* @returns {string} The formatted date string.
|
||||||
|
*/
|
||||||
|
convertTimestampDate(timestamp: number) {
|
||||||
|
const currentDate = new Date();
|
||||||
|
const inputDate = new Date(timestamp * 1000);
|
||||||
|
|
||||||
|
const days = [
|
||||||
|
'Sunday',
|
||||||
|
'Monday',
|
||||||
|
'Tuesday',
|
||||||
|
'Wednesday',
|
||||||
|
'Thursday',
|
||||||
|
'Friday',
|
||||||
|
'Saturday',
|
||||||
|
];
|
||||||
|
const months = [
|
||||||
|
'Jan.',
|
||||||
|
'Feb.',
|
||||||
|
'Mar.',
|
||||||
|
'Apr.',
|
||||||
|
'May.',
|
||||||
|
'Jun.',
|
||||||
|
'Jul.',
|
||||||
|
'Aug.',
|
||||||
|
'Sep.',
|
||||||
|
'Oct.',
|
||||||
|
'Nov.',
|
||||||
|
'Dec.',
|
||||||
|
];
|
||||||
|
|
||||||
|
const dayNumber = inputDate.getDate();
|
||||||
|
const day = days[inputDate.getDay()];
|
||||||
|
const month = months[inputDate.getMonth()];
|
||||||
|
|
||||||
|
if (inputDate.toDateString() === currentDate.toDateString()) {
|
||||||
|
return `Today`;
|
||||||
|
} else {
|
||||||
|
return `${day}, ${dayNumber} ${month}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
<section (mouseleave)="mouseLeave()">
|
||||||
|
<div [ngClass]="{ posiotionDataFromPc: hasFile, '': !hasFile }">
|
||||||
|
@if(hasFile){
|
||||||
|
<div
|
||||||
|
class="fileBox"
|
||||||
|
[ngClass]="{ dBlock: hasFile, dNone: !hasFile }"
|
||||||
|
*ngFor="let file of this.downloadFilesService.uploadFiles"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src=" {{ checkIcon(file) }}"
|
||||||
|
class="fileIcons"
|
||||||
|
(click)="showCurrentFile(file)"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="./assets/img/closeIcon.svg"
|
||||||
|
class="closeIcon"
|
||||||
|
(click)="deleteFile(file)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
#fileInput
|
||||||
|
id="file"
|
||||||
|
style="display: none"
|
||||||
|
(change)="onFileChange($event)"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
#textarea
|
||||||
|
placeholder=" {{ 'channel-msg-box.placeholder' | translate }}"
|
||||||
|
type="text"
|
||||||
|
name="textarea"
|
||||||
|
[(ngModel)]="textArea"
|
||||||
|
(keyup)="sendMessageWithEnter($event); checkChannelAndUser($event)"
|
||||||
|
></textarea>
|
||||||
|
<div class="positionAllIcons">
|
||||||
|
<div class="positionSpecialIcons">
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'addIcon.svg'"
|
||||||
|
[imgSize]="'16px'"
|
||||||
|
[btnSize]="'24px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
(click)="fileInput.click()"
|
||||||
|
></app-small-btn>
|
||||||
|
<img class="verticalLine" src="./assets/img/verticalLine.svg" />
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'smileIcon.svg'"
|
||||||
|
[imgSize]="'20px'"
|
||||||
|
[btnSize]="'32px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
(click)="toggleEmojiPicker()"
|
||||||
|
></app-small-btn>
|
||||||
|
@if (target == 'chats') {
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'atIcon.svg'"
|
||||||
|
[imgSize]="'20px'"
|
||||||
|
[btnSize]="'32px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
(click)="targetChatUser($event)"
|
||||||
|
></app-small-btn>
|
||||||
|
<div class="filteredElementWindow">
|
||||||
|
@if (toggleBoolean.selectUserInMsgBox) {
|
||||||
|
<div class="positionOfAllUsersInBox">
|
||||||
|
@for(user of userService.getFiltertUsers; track user){
|
||||||
|
<div class="user" (click)="chooseUser(user)">
|
||||||
|
<div class="positionImgs">
|
||||||
|
<img src="{{ user.avatar }}" class="avatarImg" />
|
||||||
|
<img
|
||||||
|
src="./assets/img/{{
|
||||||
|
user.status ? 'onlineRing.svg' : 'offlineRing.svg'
|
||||||
|
}}"
|
||||||
|
class="onlineIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1>{{ user.firstName }} {{ user.lastName }}</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} @if(downloadFilesService.uploadFiles.length >= 1){
|
||||||
|
<p
|
||||||
|
[ngClass]="{
|
||||||
|
showWanrningNotice: downloadFilesService.uploadFiles.length >= 1
|
||||||
|
}"
|
||||||
|
class="warningMessage"
|
||||||
|
>
|
||||||
|
{{ "channel-msg-box.text" | translate }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if(textArea == ''){
|
||||||
|
<img src="./assets/img/sendIcon.svg" class="sendIconDisabled" />
|
||||||
|
} @else {
|
||||||
|
<img
|
||||||
|
src="./assets/img/sendIcon.svg"
|
||||||
|
class="sendIcon"
|
||||||
|
(click)="sendMessage()"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<!--- show chnnels or users by pressing @ or #-->
|
||||||
|
@if (openSmallWindow) {
|
||||||
|
<div class="filteredElementWindow2">
|
||||||
|
@if (showChannels) { @for (i of checkIfUserHasAccessToChannel(); track
|
||||||
|
$index) {
|
||||||
|
<h3 class="user2" (click)="chooseElement(i)">{{ i.name }}</h3>
|
||||||
|
} }@else if (showUsers) { @for (i of userService.allUsers; track $index) {
|
||||||
|
<div class="user2" (click)="chooseElement(i)">
|
||||||
|
<div class="positionImgs2">
|
||||||
|
<img src="{{ i.avatar }}" class="avatarImg2" />
|
||||||
|
<img
|
||||||
|
src="./assets/img/{{
|
||||||
|
i.status ? 'onlineRing.svg' : 'offlineRing.svg'
|
||||||
|
}}"
|
||||||
|
class="onlineIconFilterWindow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3>{{ i.firstName }} {{ i.lastName }}</h3>
|
||||||
|
</div>
|
||||||
|
} }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (isEmojiPickerVisible) {
|
||||||
|
<app-emoji-picker
|
||||||
|
[output]="'native'"
|
||||||
|
(emojiOutputEmitter)="emojiOutputEmitter($event)"
|
||||||
|
></app-emoji-picker>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
@import "../../../../styles.scss";
|
||||||
|
|
||||||
|
section {
|
||||||
|
width: auto;
|
||||||
|
margin: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
padding-bottom: 54px;
|
||||||
|
height: 95px;
|
||||||
|
border-radius: 40px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
margin-top: 12px;
|
||||||
|
background-color: #fff;
|
||||||
|
position: relative;
|
||||||
|
> textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: "Nunito";
|
||||||
|
font-size: large;
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgb(236, 236, 236);
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.positionAllIcons {
|
||||||
|
@include displayFlex($j: space-between);
|
||||||
|
.positionSpecialIcons {
|
||||||
|
@include displayFlex($j: space-between, $g: 3px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sendIcon {
|
||||||
|
padding: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transform: 0.3 ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
scale: 1.1;
|
||||||
|
filter: brightness(0) saturate(100%) invert(56%) sepia(97%)
|
||||||
|
saturate(1270%) hue-rotate(204deg) brightness(95%) contrast(94%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sendIconDisabled{
|
||||||
|
padding: 6px;
|
||||||
|
transform: 0.3 ease-in-out;
|
||||||
|
filter: brightness(0) saturate(100%) invert(78%) sepia(4%) saturate(4098%) hue-rotate(207deg) brightness(99%) contrast(98%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verticalLine {
|
||||||
|
height: 28px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-small-btn {
|
||||||
|
padding: 6px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileBox {
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: #eee;
|
||||||
|
max-width: 40px;
|
||||||
|
max-height: 20px;
|
||||||
|
margin-right: 12px;
|
||||||
|
position: relative;
|
||||||
|
@include displayFlex();
|
||||||
|
.fileIcons {
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-right: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app-emoji-picker {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 50px;
|
||||||
|
left: 80px;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posiotionDataFromPc {
|
||||||
|
margin-top: -40px;
|
||||||
|
display: flex;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
right: -4px;
|
||||||
|
width: 12px;
|
||||||
|
object-fit: contain;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.3s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
background-color: red;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.smileysBackground {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showWanrningNotice {
|
||||||
|
animation: shwoWN 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shwoWN {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.positionOfAllUsersInBox {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 36px;
|
||||||
|
@include displayFlex($j: flex-start, $a: flex-start);
|
||||||
|
flex-direction: column;
|
||||||
|
height: 210px;
|
||||||
|
overflow: auto;
|
||||||
|
width: 240px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #745dda;
|
||||||
|
border-radius: 25px;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
padding: 8px;
|
||||||
|
.user {
|
||||||
|
@include displayFlex($g: 12px);
|
||||||
|
padding: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #edeefe;
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positionImgs {
|
||||||
|
@include displayFlex($a: flex-end);
|
||||||
|
|
||||||
|
.avatarImg {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onlineIcon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addUserBox {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warningMessage{
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filteredElementWindow2 {
|
||||||
|
z-index: 1;
|
||||||
|
max-height: 240px;
|
||||||
|
overflow: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 100px;
|
||||||
|
border-radius: 25px;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border: 1px solid #888dec;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 8px;
|
||||||
|
box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px,
|
||||||
|
rgba(0, 0, 0, 0.3) 0px 8px 16px -8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
.user2 {
|
||||||
|
@include displayFlex($j: flex-start);
|
||||||
|
padding: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.2s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
background-color: #edeefe;
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
.positionImgs2 {
|
||||||
|
@include displayFlex($a: flex-end);
|
||||||
|
.avatarImg2 {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.onlineIconFilterWindow {
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
margin-left: -10px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------- responsive ---------------
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE) {
|
||||||
|
section {
|
||||||
|
height: 50px;
|
||||||
|
padding-bottom: 54px;
|
||||||
|
margin: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warningMessage{
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ChatMsgBoxComponent } from './chat-msg-box.component';
|
||||||
|
|
||||||
|
describe('ChatMsgBoxComponent', () => {
|
||||||
|
let component: ChatMsgBoxComponent;
|
||||||
|
let fixture: ComponentFixture<ChatMsgBoxComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ChatMsgBoxComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ChatMsgBoxComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,358 @@
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { PickerComponent } from '@ctrl/ngx-emoji-mart';
|
||||||
|
import { Firestore, addDoc, collection } from '@angular/fire/firestore';
|
||||||
|
import { DownloadFilesService } from '../../../service/download-files.service';
|
||||||
|
import { UserService } from '../../../service/user.service';
|
||||||
|
import { EmojiPickerComponent } from '../../../shared/components/emoji-picker/emoji-picker.component';
|
||||||
|
import { SmallBtnComponent } from '../../../shared/components/small-btn/small-btn.component';
|
||||||
|
import { ChatService } from '../../../service/chat.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { ChannleService } from '../../../service/channle.service';
|
||||||
|
import { ToggleBooleanService } from '../../../service/toggle-boolean.service';
|
||||||
|
import { User } from '../../../interface/user.interface';
|
||||||
|
import { MessageData } from '../../../interface/chat.interface';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { Channel } from '../../../interface/channel.interface';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chat-msg-box',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
PickerComponent,
|
||||||
|
EmojiPickerComponent,
|
||||||
|
SmallBtnComponent,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
templateUrl: './chat-msg-box.component.html',
|
||||||
|
styleUrl: './chat-msg-box.component.scss',
|
||||||
|
})
|
||||||
|
export class ChatMsgBoxComponent {
|
||||||
|
@Input() currentChannel: string = '';
|
||||||
|
@Input() target: string = '';
|
||||||
|
@Output() newMsgEmitter: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||||
|
@ViewChild('textarea') textAreaRef!: ElementRef;
|
||||||
|
|
||||||
|
hasFile: boolean = false;
|
||||||
|
currentFiles!: FileList;
|
||||||
|
files: any;
|
||||||
|
getFileIcons = [
|
||||||
|
'./assets/img/documentIcon.svg',
|
||||||
|
'./assets/img/imgIcon.svg',
|
||||||
|
'./assets/img/mp3Icon.svg',
|
||||||
|
'./assets/img/pdfIcon.svg',
|
||||||
|
'./assets/img/videoIcon.svg',
|
||||||
|
];
|
||||||
|
textArea: string = '';
|
||||||
|
isEmojiPickerVisible: boolean | undefined;
|
||||||
|
currentChatValue: string = '';
|
||||||
|
showTargetMember: boolean = true;
|
||||||
|
showChannels: boolean = false;
|
||||||
|
showUsers: boolean = false;
|
||||||
|
openSmallWindow: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: Router,
|
||||||
|
public downloadFilesService: DownloadFilesService,
|
||||||
|
private firestore: Firestore,
|
||||||
|
public userService: UserService,
|
||||||
|
private chatService: ChatService,
|
||||||
|
public channelService: ChannleService,
|
||||||
|
public toggleBoolean: ToggleBooleanService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the output from the emoji picker.
|
||||||
|
* @param $event The selected emoji.
|
||||||
|
*/
|
||||||
|
emojiOutputEmitter($event: any) {
|
||||||
|
this.addEmoji($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select Textarea at onload.
|
||||||
|
*/
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.textAreaRef.nativeElement.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles file input change event.
|
||||||
|
* @param event The file change event.
|
||||||
|
*/
|
||||||
|
onFileChange(event: any) {
|
||||||
|
if (this.downloadFilesService.uploadFiles.length < 1) {
|
||||||
|
this.currentFiles = event.target.files;
|
||||||
|
this.hasFile = this.currentFiles!.length > 0;
|
||||||
|
if (this.currentFiles) {
|
||||||
|
for (let i = 0; i < this.currentFiles.length; i++) {
|
||||||
|
const fileInfo = this.currentFiles[i];
|
||||||
|
this.downloadFilesService.uploadFiles.push(fileInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the file type and returns the corresponding icon.
|
||||||
|
* @param fileInfo The file object.
|
||||||
|
* @returns The file icon path.
|
||||||
|
*/
|
||||||
|
checkIcon(fileInfo: any) {
|
||||||
|
if (fileInfo.type == 'audio/mpeg') {
|
||||||
|
return this.getFileIcons[2];
|
||||||
|
} else if (fileInfo.type == 'image/jpeg') {
|
||||||
|
return this.getFileIcons[1];
|
||||||
|
} else if (fileInfo.type == 'application/pdf') {
|
||||||
|
return this.getFileIcons[3];
|
||||||
|
} else if (fileInfo.type == 'video/mp4') {
|
||||||
|
return this.getFileIcons[4];
|
||||||
|
} else {
|
||||||
|
return this.getFileIcons[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the selected file.
|
||||||
|
* @param file The file to be deleted.
|
||||||
|
*/
|
||||||
|
deleteFile(file: File) {
|
||||||
|
const index = this.downloadFilesService.uploadFiles.indexOf(file);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.downloadFilesService.uploadFiles.splice(index, 1);
|
||||||
|
this.hasFile = this.downloadFilesService.uploadFiles.length > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the selected file in a new tab.
|
||||||
|
* @param file The file to be opened.
|
||||||
|
*/
|
||||||
|
showCurrentFile(file: File) {
|
||||||
|
const blob = new Blob([file], { type: file.type });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the selected emoji to the message text area.
|
||||||
|
* @param event The selected emoji.
|
||||||
|
*/
|
||||||
|
public addEmoji(event: any) {
|
||||||
|
this.textArea = `${this.textArea}${event}`;
|
||||||
|
this.isEmojiPickerVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of the emoji picker.
|
||||||
|
*/
|
||||||
|
toggleEmojiPicker() {
|
||||||
|
this.isEmojiPickerVisible = !this.isEmojiPickerVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the list of target chat users.
|
||||||
|
* @param event The event object.
|
||||||
|
*/
|
||||||
|
targetChatUser(event: Event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.toggleBoolean.selectUserInMsgBox = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends the selected user's name to the message text area.
|
||||||
|
* @param user The selected user.
|
||||||
|
*/
|
||||||
|
chooseUser(user: User) {
|
||||||
|
const userName = ` @${user.firstName} ${user.lastName} `;
|
||||||
|
|
||||||
|
this.textArea += userName;
|
||||||
|
this.toggleBoolean.selectUserInMsgBox = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message when Enter key is pressed.
|
||||||
|
* @param e The keyboard event.
|
||||||
|
*/
|
||||||
|
sendMessageWithEnter(e: KeyboardEvent) {
|
||||||
|
if (this.textArea.trim() !== '') {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
this.sendMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the message to the target channel.
|
||||||
|
*/
|
||||||
|
async sendMessage() {
|
||||||
|
if (this.currentChannel && this.textArea.trim() !== '') {
|
||||||
|
const messageRef = collection(this.firestore, this.target);
|
||||||
|
const messageData = this.checkCollection(this.target);
|
||||||
|
if (messageData) {
|
||||||
|
await addDoc(messageRef, messageData)
|
||||||
|
.then((docRef) => {
|
||||||
|
this.downloadFilesService.loadAllFiles(docRef.id);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error adding document: ', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Invalid target:', this.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.forwardToChannel();
|
||||||
|
this.resetValues();
|
||||||
|
this.newMsgEmitter.emit(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the target collection and returns the message data.
|
||||||
|
* @param target The target collection.
|
||||||
|
* @returns The message data.
|
||||||
|
*/
|
||||||
|
checkCollection(target: string): MessageData | null {
|
||||||
|
let messageData: Partial<MessageData> = {
|
||||||
|
message: this.textArea,
|
||||||
|
publishedTimestamp: Math.floor(Date.now() / 1000),
|
||||||
|
userId: this.userService.getCurrentUserId(),
|
||||||
|
edited: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (target === 'chats') {
|
||||||
|
messageData.channelId = this.checkChannelId();
|
||||||
|
} else if (target === 'chat-answers') {
|
||||||
|
messageData.chatId = this.checkChatId();
|
||||||
|
} else {
|
||||||
|
console.error('Invalid target:', target);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return messageData as MessageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks
|
||||||
|
* the channel ID based on the chat service.
|
||||||
|
* @returns The channel ID.
|
||||||
|
*/
|
||||||
|
checkChannelId() {
|
||||||
|
if (this.chatService.getChannelId) {
|
||||||
|
return this.chatService.getChannelId;
|
||||||
|
} else if (this.chatService.getPrvChatId) {
|
||||||
|
return this.chatService.getPrvChatId;
|
||||||
|
} else if (
|
||||||
|
this.currentChannel === 'searchBar' &&
|
||||||
|
this.chatService.inputValue === ''
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return this.currentChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the chat ID based on the chat service.
|
||||||
|
* @returns The chat ID.
|
||||||
|
*/
|
||||||
|
checkChatId() {
|
||||||
|
if (this.chatService.isSecondaryChatId) {
|
||||||
|
return this.chatService.isSecondaryChatId;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to the target channel after sending the message.
|
||||||
|
*/
|
||||||
|
forwardToChannel() {
|
||||||
|
if (this.chatService.getChannelId || this.chatService.getPrvChatId) {
|
||||||
|
this.route.navigateByUrl(`/main/${this.checkChannelId()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close popups by leaving with the mouse the chat-msg-box.
|
||||||
|
*/
|
||||||
|
mouseLeave() {
|
||||||
|
this.isEmojiPickerVisible = false;
|
||||||
|
this.toggleBoolean.selectUserInMsgBox = false;
|
||||||
|
this.openSmallWindow = false;
|
||||||
|
this.showChannels = false;
|
||||||
|
this.showUsers = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open channels or user window by pressing @ or #.
|
||||||
|
*/
|
||||||
|
checkChannelAndUser(e: KeyboardEvent) {
|
||||||
|
if (e.key === '#') {
|
||||||
|
this.openSmallWindow = true;
|
||||||
|
this.showChannels = true;
|
||||||
|
this.showUsers = false;
|
||||||
|
} else if (e.key === '@') {
|
||||||
|
this.openSmallWindow = true;
|
||||||
|
this.showChannels = false;
|
||||||
|
this.showUsers = true;
|
||||||
|
}
|
||||||
|
if (e.key === 'Backspace') {
|
||||||
|
this.openSmallWindow = false;
|
||||||
|
this.showChannels = false;
|
||||||
|
this.showUsers = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current user has access to any channels.
|
||||||
|
* @returns {Channel[]} Array of Channel objects that the user has access to.
|
||||||
|
*/
|
||||||
|
checkIfUserHasAccessToChannel() {
|
||||||
|
const isUserAChannelMember = this.channelService.allChannels.some(
|
||||||
|
(channel) =>
|
||||||
|
channel.addedUser.includes(this.userService.getCurrentUserId())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isUserAChannelMember) {
|
||||||
|
return this.channelService.allChannels.filter((channel) =>
|
||||||
|
channel.addedUser.includes(this.userService.getCurrentUserId())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses an element and performs necessary actions based on its type.
|
||||||
|
* @param {Channel | User} element - The element to choose.
|
||||||
|
*/
|
||||||
|
chooseElement(element: Channel | User) {
|
||||||
|
if ('firstName' in element) {
|
||||||
|
this.textArea += `${element.firstName} ${element.lastName} `;
|
||||||
|
} else {
|
||||||
|
this.textArea += `${element.name} `;
|
||||||
|
}
|
||||||
|
this.showUsers = false;
|
||||||
|
this.showChannels = false;
|
||||||
|
this.openSmallWindow = false;
|
||||||
|
this.textAreaRef.nativeElement.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets input values after sending the message.
|
||||||
|
*/
|
||||||
|
resetValues() {
|
||||||
|
this.textArea = '';
|
||||||
|
this.downloadFilesService.uploadFiles = [];
|
||||||
|
this.hasFile = false;
|
||||||
|
this.chatService.inputValue = '';
|
||||||
|
this.chatService.getChannelId = '';
|
||||||
|
this.chatService.getPrvChatId = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/app/components/main-chat/info/info.component.html
Normal file
75
src/app/components/main-chat/info/info.component.html
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<section>
|
||||||
|
<!-- Channel -->
|
||||||
|
@if (getChannel(currentChannel).length > 0) {
|
||||||
|
<div class="headline"># {{ getChannel(currentChannel)[0].name }}</div>
|
||||||
|
<p>
|
||||||
|
{{ "info.channelCreate" | translate }}
|
||||||
|
@if(getChannel(currentChannel)[0].creator == "Admin") { Admin } @else { @for
|
||||||
|
(user of getChatUsers(getChannel(currentChannel)[0].creator); track user) {
|
||||||
|
{{ user.firstName }}
|
||||||
|
{{ user.lastName }}
|
||||||
|
} } {{ "info.channelCreate2" | translate }}
|
||||||
|
{{ timeConverter(getChannel(currentChannel)[0].createdDate)
|
||||||
|
}}{{ "info.channelCreate3" | translate }}
|
||||||
|
<span>#{{ getChannel(currentChannel)[0].name }}</span
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<!-- Privat Chat -->
|
||||||
|
} @if (getPrivatChannel(currentChannel).length > 0) {
|
||||||
|
<!-- My Chat -->
|
||||||
|
@if (getPrivatChannel(currentChannel)[0].talkToUserId ==
|
||||||
|
userService.getCurrentUserId() &&
|
||||||
|
getPrivatChannel(currentChannel)[0].creatorId ==
|
||||||
|
userService.getCurrentUserId()) {
|
||||||
|
<div class="headlinePrvChat">
|
||||||
|
@for (user of getChatUsers(getPrivatChannel(currentChannel)[0].creatorId);
|
||||||
|
track user) {
|
||||||
|
<div class="imgBoxPrvChat">
|
||||||
|
<img class="imgUserPrvChat" src="{{ user.avatar }}" />
|
||||||
|
</div>
|
||||||
|
<p>{{ user.firstName }} {{ user.lastName }} (Du)</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{{ "info.notice" | translate }}
|
||||||
|
</p>
|
||||||
|
} @else {
|
||||||
|
<!-- Privat Chat (creatorId) -->
|
||||||
|
@if (getPrivatChannel(currentChannel)[0].talkToUserId ==
|
||||||
|
userService.getCurrentUserId()) {
|
||||||
|
<div class="headlinePrvChat">
|
||||||
|
@for (user of getChatUsers(getPrivatChannel(currentChannel)[0].creatorId);
|
||||||
|
track user) {
|
||||||
|
<div class="imgBoxPrvChat">
|
||||||
|
<img class="imgUserPrvChat" src="{{ user.avatar }}" />
|
||||||
|
</div>
|
||||||
|
<p>{{ user.firstName }} {{ user.lastName }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{{ "info.talkTo" | translate }} @for (user of
|
||||||
|
getChatUsers(getPrivatChannel(currentChannel)[0].creatorId); track user) {
|
||||||
|
<span>@{{ user.firstName }}{{ user.lastName }}</span>
|
||||||
|
} {{ "info.talkTo2" | translate }}
|
||||||
|
</p>
|
||||||
|
} @else {
|
||||||
|
<!-- Privat Chat (talkToUserId) -->
|
||||||
|
<div class="headlinePrvChat">
|
||||||
|
@for (user of
|
||||||
|
getChatUsers(getPrivatChannel(currentChannel)[0].talkToUserId); track user)
|
||||||
|
{
|
||||||
|
<div class="imgBoxPrvChat">
|
||||||
|
<img class="imgUserPrvChat" src="{{ user.avatar }}" />
|
||||||
|
</div>
|
||||||
|
<p>{{ user.firstName }} {{ user.lastName }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{{ "info.talkTo" | translate }} @for (user of
|
||||||
|
getChatUsers(getPrivatChannel(currentChannel)[0].talkToUserId); track user)
|
||||||
|
{
|
||||||
|
<span>@{{ user.firstName }} {{ user.lastName }}</span>
|
||||||
|
} {{ "info.talkTo2" | translate }}
|
||||||
|
</p>
|
||||||
|
} } }
|
||||||
|
</section>
|
||||||
70
src/app/components/main-chat/info/info.component.scss
Normal file
70
src/app/components/main-chat/info/info.component.scss
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
@import "../../../../styles.scss";
|
||||||
|
|
||||||
|
section {
|
||||||
|
padding: 32px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: #535af1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headlinePrvChat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
max-width: fit-content;
|
||||||
|
.imgBoxPrvChat {
|
||||||
|
display: flex;
|
||||||
|
margin-right: 16px;
|
||||||
|
.imgUserPrvChat {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------- RESPONSIVE -------------*/
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE) {
|
||||||
|
section {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headlinePrvChat {
|
||||||
|
.imgBoxPrvChat {
|
||||||
|
margin-right: 12px;
|
||||||
|
.imgUserPrvChat {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/app/components/main-chat/info/info.component.spec.ts
Normal file
23
src/app/components/main-chat/info/info.component.spec.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { InfoComponent } from './info.component';
|
||||||
|
|
||||||
|
describe('InfoComponent', () => {
|
||||||
|
let component: InfoComponent;
|
||||||
|
let fixture: ComponentFixture<InfoComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [InfoComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(InfoComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
84
src/app/components/main-chat/info/info.component.ts
Normal file
84
src/app/components/main-chat/info/info.component.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { ChannleService } from '../../../service/channle.service';
|
||||||
|
import { UserService } from '../../../service/user.service';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-info',
|
||||||
|
standalone: true,
|
||||||
|
imports: [TranslateModule],
|
||||||
|
templateUrl: './info.component.html',
|
||||||
|
styleUrl: './info.component.scss',
|
||||||
|
})
|
||||||
|
export class InfoComponent {
|
||||||
|
@Input() currentChannel: string = '';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private channelService: ChannleService,
|
||||||
|
public userService: UserService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves chat users based on the provided chat ID.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @returns {Array} - An array containing the users associated with the chat.
|
||||||
|
*/
|
||||||
|
getChatUsers(chatId: string) {
|
||||||
|
const filteredTasks = this.userService.allUsers.filter(
|
||||||
|
(user) => user.id == chatId
|
||||||
|
);
|
||||||
|
return filteredTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a channel based on the provided chat ID.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @returns {Array} - An array containing the channel matching the provided chat ID.
|
||||||
|
*/
|
||||||
|
getChannel(chatId: string) {
|
||||||
|
const filteredTasks = this.channelService.allChannels.filter(
|
||||||
|
(channel) => channel.id == chatId
|
||||||
|
);
|
||||||
|
return filteredTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a private channel based on the provided chat ID.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @returns {Array} - An array containing the private channel matching the provided chat ID.
|
||||||
|
*/
|
||||||
|
getPrivatChannel(chatId: string) {
|
||||||
|
const filteredTasks = this.channelService.allPrvChannels.filter(
|
||||||
|
(channel) => channel.id == chatId
|
||||||
|
);
|
||||||
|
return filteredTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a date string to a formatted time string.
|
||||||
|
* @param {string} dateString - The date string to convert.
|
||||||
|
* @returns {string} - The formatted time string (e.g., "Jan. 1, 2022").
|
||||||
|
*/
|
||||||
|
timeConverter(dateString: string) {
|
||||||
|
var a = new Date(dateString);
|
||||||
|
var months = [
|
||||||
|
'Jan.',
|
||||||
|
'Feb.',
|
||||||
|
'Mar.',
|
||||||
|
'Apr.',
|
||||||
|
'May.',
|
||||||
|
'Jun.',
|
||||||
|
'Jul.',
|
||||||
|
'Aug.',
|
||||||
|
'Sep.',
|
||||||
|
'Oct.',
|
||||||
|
'Nov.',
|
||||||
|
'Dec.',
|
||||||
|
];
|
||||||
|
var year = a.getFullYear();
|
||||||
|
var month = months[a.getMonth()];
|
||||||
|
var date = a.getDate();
|
||||||
|
var time = month + ' ' + date + ', ' + year;
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
}
|
||||||
179
src/app/components/main-chat/main-chat.component.html
Normal file
179
src/app/components/main-chat/main-chat.component.html
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
<section>
|
||||||
|
@if (checkCurrentChannel(currentChannel) == 'searchBar') {
|
||||||
|
<!-----Searchwindow at the sidebar----->
|
||||||
|
<div class="headerSearchBar">
|
||||||
|
<p>{{ "main-chat.message" | translate }}</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="{{ 'main-chat.placeholder' | translate }}"
|
||||||
|
[(ngModel)]="chatService.inputValue"
|
||||||
|
(keyup)="filterChannelAndUser(chatService.inputValue)"
|
||||||
|
/>
|
||||||
|
@if (toggleBoolean.openSearchWindow) {
|
||||||
|
<!-----search suggestions----->
|
||||||
|
<div class="filteredElementWindow">
|
||||||
|
@if (firstLetter == '#') {
|
||||||
|
<!-- render channels -->
|
||||||
|
@for (i of checkIfUserHasAccessToChannel(); track $index) {
|
||||||
|
<h3
|
||||||
|
class="user"
|
||||||
|
(click)="chooseElement(i)"
|
||||||
|
[innerHTML]="i.name | highlight : chatService.inputValue"
|
||||||
|
></h3>
|
||||||
|
} } @else if (firstLetter == '@') {
|
||||||
|
<!-- render users -->
|
||||||
|
@for (i of userService.allUsers; track $index) {
|
||||||
|
<div class="user" (click)="chooseElement(i)">
|
||||||
|
<div class="positionImgs">
|
||||||
|
<img src="{{ i.avatar }}" class="avatarImg" />
|
||||||
|
<img
|
||||||
|
src="./assets/img/{{
|
||||||
|
i.status ? 'onlineRing.svg' : 'offlineRing.svg'
|
||||||
|
}}"
|
||||||
|
class="onlineIconFilterWindow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3
|
||||||
|
[innerHTML]="
|
||||||
|
i.firstName + ' ' + i.lastName | highlight : chatService.inputValue
|
||||||
|
"
|
||||||
|
></h3>
|
||||||
|
</div>
|
||||||
|
} }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
[ngStyle]="{
|
||||||
|
'margin-top': viewWidth <= RESPONSIVE_THRESHOLD_MOBILE ? '-88px' : '-64px'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<app-chat-content
|
||||||
|
[getChatChannel]="getChatChannel"
|
||||||
|
[getChatUsers]="getChatUsers"
|
||||||
|
[currentChannel]="currentChannel"
|
||||||
|
[hideContentWindow]="false"
|
||||||
|
[isSearchChannel]="true"
|
||||||
|
>
|
||||||
|
</app-chat-content>
|
||||||
|
</div>
|
||||||
|
} @else { @if(checkCurrentChannel(currentChannel) == 'allPrvChannels'){
|
||||||
|
<!------------shwo prv chat----------->
|
||||||
|
@for (user of getPrvChat(currentChannel); track user; let i = $index) {
|
||||||
|
<div class="headerPrvChat">
|
||||||
|
<div class="headlinePrvChat" (click)="openUserProfil()">
|
||||||
|
@if (user.talkToUserId == userService.getCurrentUserId()) {
|
||||||
|
<div class="imgBoxPrvChat">
|
||||||
|
<img
|
||||||
|
class="imgUserPrvChat"
|
||||||
|
src="{{ filterUser(user.creatorId)[i].avatar }}"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="./assets/img/{{
|
||||||
|
filterUser(user.creatorId)[i].status
|
||||||
|
? 'onlineRing.svg'
|
||||||
|
: 'offlineRing.svg'
|
||||||
|
}}"
|
||||||
|
class="onlineIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{{ filterUser(user.creatorId)[i].firstName }}
|
||||||
|
{{ filterUser(user.creatorId)[i].lastName }}
|
||||||
|
</p>
|
||||||
|
} @else {
|
||||||
|
<div class="imgBoxPrvChat">
|
||||||
|
<img
|
||||||
|
class="imgUserPrvChat"
|
||||||
|
src="{{ filterUser(user.talkToUserId)[i].avatar }}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{{ filterUser(user.talkToUserId)[i].firstName }}
|
||||||
|
{{ filterUser(user.talkToUserId)[i].lastName }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-chat-content
|
||||||
|
[getChatChannel]="getChatChannel"
|
||||||
|
[getChatUsers]="getChatUsers"
|
||||||
|
[currentChannel]="currentChannel"
|
||||||
|
[isPrivatChannel]="true"
|
||||||
|
[hideContentWindow]="false"
|
||||||
|
[viewWidth]="viewWidth"
|
||||||
|
>
|
||||||
|
</app-chat-content>
|
||||||
|
} } @else if (checkCurrentChannel(currentChannel) == 'allChannels') {
|
||||||
|
<!-------show public channel-chat ----------->
|
||||||
|
@for(channel of getChannelName(currentChannel); track channel) {
|
||||||
|
<div class="header">
|
||||||
|
<div class="headline" (click)="showMenu()">
|
||||||
|
<img src="./assets/img/hashtag.svg" alt="" />
|
||||||
|
<p>{{ channel.name }}</p>
|
||||||
|
<img src="./assets/img/keyboardArrowDown.svg" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="addUserToChannelBox">
|
||||||
|
@if (filterChannelForSelectedUser(currentChannel)) {
|
||||||
|
<div class="addedUsers" (click)="openMemberWindow(false)">
|
||||||
|
@for(user of userService.getFiltertUsers.slice(0, 3) ; track user) {
|
||||||
|
<div class="userAvatar">
|
||||||
|
<img src="{{ user.avatar }}" alt="" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div *ngIf="userService.getFiltertUsers.length > 3" class="dotsImg">
|
||||||
|
<img
|
||||||
|
*ngFor="let i of [0, 1, 2]"
|
||||||
|
src="./assets/img/offlineRing.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3>{{ userService.getFiltertUsers.length }}</h3>
|
||||||
|
</div>
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'addPerson.svg'"
|
||||||
|
[imgSize]="'20px'"
|
||||||
|
[btnSize]="'32px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
(click)="
|
||||||
|
viewWidth <= RESPONSIVE_THRESHOLD_MOBILE
|
||||||
|
? openMemberWindow(false)
|
||||||
|
: openMemberWindow(true)
|
||||||
|
"
|
||||||
|
></app-small-btn>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-show-channel-member
|
||||||
|
[ngClass]="{
|
||||||
|
dBlock: toggleBoolean.openChannelMemberWindow,
|
||||||
|
dNone: !toggleBoolean.openChannelMemberWindow
|
||||||
|
}"
|
||||||
|
[getFiltertUsers]="userService.getFiltertUsers"
|
||||||
|
[currentChannel]="currentChannel"
|
||||||
|
[isSecondaryChatOpen]="isSecondaryChatOpen"
|
||||||
|
></app-show-channel-member>
|
||||||
|
<app-chat-content
|
||||||
|
[getChatChannel]="getChatChannel"
|
||||||
|
[getChatUsers]="getChatUsers"
|
||||||
|
[currentChannel]="currentChannel"
|
||||||
|
[hideContentWindow]="false"
|
||||||
|
[viewWidth]="viewWidth"
|
||||||
|
>
|
||||||
|
</app-chat-content>
|
||||||
|
}
|
||||||
|
<!-- Channel informations dialog -->
|
||||||
|
@if (openMenu) {
|
||||||
|
<app-channel-informations
|
||||||
|
[viewWidth]="viewWidth"
|
||||||
|
(closeEditEmitter)="closeEditEmitter($event)"
|
||||||
|
[currentChannel]="currentChannel"
|
||||||
|
></app-channel-informations>
|
||||||
|
} } } @if (showProfil) {
|
||||||
|
<app-open-send-prv-message-window
|
||||||
|
[showProfil]="showProfil"
|
||||||
|
(showProfilWindow)="getShowProfilWindowBoolean($event)"
|
||||||
|
[talkToUser]="talkToUser"
|
||||||
|
></app-open-send-prv-message-window>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
296
src/app/components/main-chat/main-chat.component.scss
Normal file
296
src/app/components/main-chat/main-chat.component.scss
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
@import "./../../../styles.scss";
|
||||||
|
|
||||||
|
section {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 24px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
height: 40px;
|
||||||
|
padding: 24px 48px;
|
||||||
|
box-shadow: 0px 3px 3px 0px rgba(0, 0, 0, 0.05);
|
||||||
|
@include displayFlex($j: space-between);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
max-width: fit-content;
|
||||||
|
cursor: pointer;
|
||||||
|
img {
|
||||||
|
&:first-child {
|
||||||
|
width: 24px;
|
||||||
|
height: 25px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
width: 14px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: #edeefe;
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addUserToChannelBox {
|
||||||
|
@include displayFlex($g: 12px);
|
||||||
|
.addedUsers {
|
||||||
|
@include displayFlex($g: 8px);
|
||||||
|
width: auto;
|
||||||
|
height: 40px;
|
||||||
|
padding: 3px;
|
||||||
|
padding-left: 26px;
|
||||||
|
border-radius: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
background-color: #edeefe;
|
||||||
|
}
|
||||||
|
.userAvatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
background-color: whitesmoke;
|
||||||
|
border-radius: 50%;
|
||||||
|
@include displayFlex();
|
||||||
|
margin-left: -22px;
|
||||||
|
img {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- prv chat--------
|
||||||
|
|
||||||
|
.headerPrvChat {
|
||||||
|
height: 60px;
|
||||||
|
padding: 24px 48px;
|
||||||
|
box-shadow: 0px 3px 3px 0px rgba(0, 0, 0, 0.05);
|
||||||
|
@include displayFlex($j: space-between);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerBoxPrvChat {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headlinePrvChat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
max-width: fit-content;
|
||||||
|
cursor: pointer;
|
||||||
|
.imgBoxPrvChat {
|
||||||
|
@include displayFlex($a: flex-end);
|
||||||
|
margin-right: 16px;
|
||||||
|
.imgUserPrvChat {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.onlineIcon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: -12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: #edeefe;
|
||||||
|
border-radius: 35px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerSearchBar {
|
||||||
|
height: auto;
|
||||||
|
border-top-left-radius: 25px;
|
||||||
|
border-top-right-radius: 25px;
|
||||||
|
box-shadow: 0px 3px 3px 0px rgba(0, 0, 0, 0.05);
|
||||||
|
@include displayFlex($a: flex-start);
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 24px 48px;
|
||||||
|
position: relative;
|
||||||
|
p {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
height: 55px;
|
||||||
|
border-radius: 26px;
|
||||||
|
padding-left: 24px;
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
border: 1px solid #888dec;
|
||||||
|
font-family: "Nunito", sans-serif;
|
||||||
|
outline: none;
|
||||||
|
&:hover {
|
||||||
|
border: 1px solid #5f66e7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filteredElementWindow {
|
||||||
|
z-index: 1;
|
||||||
|
max-height: 240px;
|
||||||
|
overflow: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 127px;
|
||||||
|
left: 80px;
|
||||||
|
border-radius: 25px;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border: 1px solid #888dec;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 8px;
|
||||||
|
box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px,
|
||||||
|
rgba(0, 0, 0, 0.3) 0px 8px 16px -8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
.user {
|
||||||
|
@include displayFlex($j: flex-start);
|
||||||
|
padding: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.2s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
background-color: #edeefe;
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
.positionImgs {
|
||||||
|
@include displayFlex($a: flex-end);
|
||||||
|
.avatarImg {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.onlineIconFilterWindow {
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
margin-left: -10px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dotsImg {
|
||||||
|
@include displayFlex($a: flex-end);
|
||||||
|
margin-bottom: -26px;
|
||||||
|
margin-left: -10px;
|
||||||
|
img {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------- RESPONSIVE -------------*/
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE) {
|
||||||
|
section {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addUserToChannelBox {
|
||||||
|
.addedUsers {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
height: 36px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerPrvChat {
|
||||||
|
height: 54px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
p {
|
||||||
|
font-size: 20px;
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
&:first-child {
|
||||||
|
width: 18px;
|
||||||
|
height: 19px;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerSearchBar {
|
||||||
|
height: 84px;
|
||||||
|
padding: 12px;
|
||||||
|
p {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
width: calc(100% - 28px);
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.headlinePrvChat {
|
||||||
|
.imgBoxPrvChat {
|
||||||
|
margin-right: 16px;
|
||||||
|
.imgUserPrvChat {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
.onlineIcon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: -12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: #edeefe;
|
||||||
|
border-radius: 35px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------- RESPONSIVE -------------*/
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE) {
|
||||||
|
.filteredElementWindow {
|
||||||
|
top: 95px;
|
||||||
|
left: 45px;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/app/components/main-chat/main-chat.component.spec.ts
Normal file
23
src/app/components/main-chat/main-chat.component.spec.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MainChatComponent } from './main-chat.component';
|
||||||
|
|
||||||
|
describe('MainChatComponent', () => {
|
||||||
|
let component: MainChatComponent;
|
||||||
|
let fixture: ComponentFixture<MainChatComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MainChatComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(MainChatComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
365
src/app/components/main-chat/main-chat.component.ts
Normal file
365
src/app/components/main-chat/main-chat.component.ts
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
import { Component, ElementRef, Input } from '@angular/core';
|
||||||
|
import { ChannleService } from '../../service/channle.service';
|
||||||
|
import { MainComponent } from '../main/main.component';
|
||||||
|
import { ChatService } from '../../service/chat.service';
|
||||||
|
import { UserService } from '../../service/user.service';
|
||||||
|
import { User } from '../../interface/user.interface';
|
||||||
|
import {
|
||||||
|
Channel,
|
||||||
|
PrvChannel,
|
||||||
|
publicChannels,
|
||||||
|
} from '../../interface/channel.interface';
|
||||||
|
import { Chat } from '../../interface/chat.interface';
|
||||||
|
import { NavigationStart, Router } from '@angular/router';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ChatContentComponent } from './chat-content/chat-content.component';
|
||||||
|
import { SingleChatComponent } from './single-chat/single-chat.component';
|
||||||
|
import { ToggleBooleanService } from '../../service/toggle-boolean.service';
|
||||||
|
import { ChatMsgBoxComponent } from './chat-msg-box/chat-msg-box.component';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { SmallBtnComponent } from '../../shared/components/small-btn/small-btn.component';
|
||||||
|
import { ShowChannelMemberComponent } from './show-channel-member/show-channel-member.component';
|
||||||
|
import { SharedService } from '../../service/shared.service';
|
||||||
|
import { ChannelInformationsComponent } from './channel-informations/channel-informations.component';
|
||||||
|
import { filter } from 'rxjs';
|
||||||
|
import { OpenSendPrvMessageWindowComponent } from './show-channel-member/open-send-prv-message-window/open-send-prv-message-window.component';
|
||||||
|
import { HighlightPipe } from '../../highlight.pipe';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-main-chat',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
MainComponent,
|
||||||
|
CommonModule,
|
||||||
|
ChatContentComponent,
|
||||||
|
SingleChatComponent,
|
||||||
|
ChatMsgBoxComponent,
|
||||||
|
FormsModule,
|
||||||
|
SmallBtnComponent,
|
||||||
|
ShowChannelMemberComponent,
|
||||||
|
ChannelInformationsComponent,
|
||||||
|
OpenSendPrvMessageWindowComponent,
|
||||||
|
HighlightPipe,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
templateUrl: './main-chat.component.html',
|
||||||
|
styleUrl: './main-chat.component.scss',
|
||||||
|
})
|
||||||
|
export class MainChatComponent {
|
||||||
|
@Input() currentChannel: string = '';
|
||||||
|
@Input() isSecondaryChatOpen: boolean = false;
|
||||||
|
@Input() viewWidth: number = 0;
|
||||||
|
|
||||||
|
firstLetter: string = '';
|
||||||
|
openSearchWindow: boolean = false;
|
||||||
|
channelCreator: boolean = false;
|
||||||
|
openMenu: boolean = false;
|
||||||
|
showProfil: boolean = false;
|
||||||
|
talkToUser!: User[];
|
||||||
|
routToPrvCHannel: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: Router,
|
||||||
|
public userService: UserService,
|
||||||
|
public channelService: ChannleService,
|
||||||
|
public chatService: ChatService,
|
||||||
|
public toggleBoolean: ToggleBooleanService,
|
||||||
|
private sharedService: SharedService
|
||||||
|
) {
|
||||||
|
this.checkPrivatChatRouteEvent();
|
||||||
|
this.routeToStartChannel();
|
||||||
|
}
|
||||||
|
|
||||||
|
RESPONSIVE_THRESHOLD_MOBILE = this.sharedService.RESPONSIVE_THRESHOLD_MOBILE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the openMenu property based on the provided variable.
|
||||||
|
* @param {boolean} variable - The value to set for openMenu.
|
||||||
|
*/
|
||||||
|
closeEditEmitter(variable: boolean) {
|
||||||
|
this.openMenu = variable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the showProfil property based on the provided value.
|
||||||
|
* @param {boolean} value - The value to set for showProfil.
|
||||||
|
*/
|
||||||
|
getShowProfilWindowBoolean(value: boolean) {
|
||||||
|
this.showProfil = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the route events for private chat messages.
|
||||||
|
*/
|
||||||
|
checkPrivatChatRouteEvent() {
|
||||||
|
this.route.events
|
||||||
|
.pipe(
|
||||||
|
filter(
|
||||||
|
(event): event is NavigationStart => event instanceof NavigationStart
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribe((event: NavigationStart) => {
|
||||||
|
const urlParts = this.route.url.split('/');
|
||||||
|
const id = urlParts[urlParts.length - 1];
|
||||||
|
this.hasPrivatChatMessages(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if there are private chat messages for a given chat ID and performs necessary actions.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
*/
|
||||||
|
hasPrivatChatMessages(chatId: string) {
|
||||||
|
const isPrivatChannel = this.channelService.allPrvChannels.filter(
|
||||||
|
(user) => user.id === chatId
|
||||||
|
);
|
||||||
|
if (isPrivatChannel.length > 0) {
|
||||||
|
const countMessages = this.chatService.allChats.filter(
|
||||||
|
(user) => user.channelId === chatId
|
||||||
|
);
|
||||||
|
const userChannel = this.channelService.allPrvChannels.filter(
|
||||||
|
(user) =>
|
||||||
|
user.creatorId === this.userService.getCurrentUserId() &&
|
||||||
|
user.talkToUserId === this.userService.getCurrentUserId() &&
|
||||||
|
user.id === chatId
|
||||||
|
);
|
||||||
|
if (countMessages.length === 0 && userChannel.length === 0) {
|
||||||
|
this.chatService.deleteData(chatId, 'prv-channels');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects to the start channel if the current channel is empty and the user is logged in.
|
||||||
|
*/
|
||||||
|
routeToStartChannel() {
|
||||||
|
if (
|
||||||
|
this.currentChannel === '' &&
|
||||||
|
this.userService.getCurrentUserId() !== undefined
|
||||||
|
) {
|
||||||
|
this.route.navigateByUrl(`/main/${publicChannels[0]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current user has access to any channels.
|
||||||
|
* @returns {Channel[]} Array of Channel objects that the user has access to.
|
||||||
|
*/
|
||||||
|
checkIfUserHasAccessToChannel() {
|
||||||
|
const isUserAChannelMember = this.channelService.allChannels.some(
|
||||||
|
(channel) =>
|
||||||
|
channel.addedUser.includes(this.userService.getCurrentUserId())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isUserAChannelMember) {
|
||||||
|
return this.channelService.allChannels.filter((channel) =>
|
||||||
|
channel.addedUser.includes(this.userService.getCurrentUserId())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the openMenu property to true.
|
||||||
|
*/
|
||||||
|
showMenu() {
|
||||||
|
this.openMenu = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves users associated with a chat ID.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @returns {User[]} Array of User objects associated with the chat ID.
|
||||||
|
*/
|
||||||
|
getChatUsers(chatId: string) {
|
||||||
|
const filteredTasks = this.userService.allUsers.filter(
|
||||||
|
(user) => user.id == chatId
|
||||||
|
);
|
||||||
|
return filteredTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves chats associated with a chat ID.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @returns {Chat[]} Array of Chat objects associated with the chat ID.
|
||||||
|
*/
|
||||||
|
getChatChannel(chatId: string) {
|
||||||
|
const filteredTasks = this.chatService.allChats.filter(
|
||||||
|
(chat) => chat.channelId == chatId
|
||||||
|
);
|
||||||
|
return filteredTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves channel information for a given channel ID.
|
||||||
|
* @param {string} chatId - The ID of the channel.
|
||||||
|
* @returns {Channel[]} Array of Channel objects matching the provided channel ID.
|
||||||
|
*/
|
||||||
|
getChannelName(chatId: string) {
|
||||||
|
const filteredTasks = this.channelService.allChannels.filter(
|
||||||
|
(channel) => channel.id == chatId
|
||||||
|
);
|
||||||
|
return filteredTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves private chat information for a given private chat ID.
|
||||||
|
* @param {string} prvChatId - The ID of the private chat.
|
||||||
|
* @returns {PrvChannel[]} Array of PrvChannel objects matching the provided private chat ID.
|
||||||
|
*/
|
||||||
|
getPrvChat(prvChatId: string) {
|
||||||
|
const filteredChats = this.channelService.allPrvChannels.filter(
|
||||||
|
(prvChat) => prvChat.id == prvChatId
|
||||||
|
);
|
||||||
|
this.getTalkToUser(filteredChats);
|
||||||
|
return filteredChats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the user associated with a filtered private chat.
|
||||||
|
* @param {PrvChannel[]} filteredChat - The filtered private chat.
|
||||||
|
*/
|
||||||
|
getTalkToUser(filteredChat: PrvChannel[]) {
|
||||||
|
const talkToUser = filteredChat[0].talkToUserId;
|
||||||
|
const getUser = this.userService.allUsers.filter(
|
||||||
|
(user) => user.id === talkToUser
|
||||||
|
);
|
||||||
|
if (getUser) {
|
||||||
|
this.talkToUser = getUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters users based on the provided talkToUserId.
|
||||||
|
* @param {string} talkToUserId - The ID of the user to filter.
|
||||||
|
* @returns {User[]} Array of User objects matching the provided user ID.
|
||||||
|
*/
|
||||||
|
filterUser(talkToUserId: string) {
|
||||||
|
return this.userService.allUsers.filter((user) => user.id == talkToUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the type of the current channel.
|
||||||
|
* @param {string} currentChannel - The ID of the current channel.
|
||||||
|
* @returns {string} Type of the current channel ('searchBar', 'allChannels', 'allPrvChannels').
|
||||||
|
*/
|
||||||
|
checkCurrentChannel(currentChannel: string) {
|
||||||
|
if (currentChannel === 'searchBar') {
|
||||||
|
return 'searchBar';
|
||||||
|
}
|
||||||
|
const allChannels = this.channelService.allChannels.some(
|
||||||
|
(channel) => channel.id == currentChannel
|
||||||
|
);
|
||||||
|
const allPrvChannels = this.channelService.allPrvChannels.some(
|
||||||
|
(channel) => channel.id == currentChannel
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allChannels) {
|
||||||
|
return 'allChannels';
|
||||||
|
} else if (allPrvChannels) {
|
||||||
|
return 'allPrvChannels';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters channels and users based on the input value.
|
||||||
|
* @param {string} inputValue - The input value to filter.
|
||||||
|
* @returns {string} Type of filtering ('filterChannel', 'filterUsers').
|
||||||
|
*/
|
||||||
|
filterChannelAndUser(inputValue: string) {
|
||||||
|
const filterChannels = '#';
|
||||||
|
const filterUsers = '@';
|
||||||
|
this.firstLetter = inputValue[0];
|
||||||
|
if (this.firstLetter == filterChannels) {
|
||||||
|
this.toggleBoolean.openSearchWindow = true;
|
||||||
|
return 'filterChannel';
|
||||||
|
} else if (this.firstLetter == filterUsers) {
|
||||||
|
this.toggleBoolean.openSearchWindow = true;
|
||||||
|
return 'filterUsers';
|
||||||
|
}
|
||||||
|
return (this.chatService.inputValue = '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the showProfil property to true.
|
||||||
|
*/
|
||||||
|
openUserProfil() {
|
||||||
|
this.showProfil = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses an element and performs necessary actions based on its type.
|
||||||
|
* @param {Channel | User} element - The element to choose.
|
||||||
|
*/
|
||||||
|
chooseElement(element: Channel | User) {
|
||||||
|
if ('firstName' in element) {
|
||||||
|
this.chatService.inputValue = `@${element.firstName} ${element.lastName}`;
|
||||||
|
const getUserID = element.id!;
|
||||||
|
this.routToPrvCHannel = true;
|
||||||
|
this.checkIfPrvChatExist(getUserID);
|
||||||
|
} else {
|
||||||
|
this.chatService.inputValue = `#${element.name}`;
|
||||||
|
this.chatService.getChannelId = element.id!;
|
||||||
|
}
|
||||||
|
this.toggleBoolean.openSearchWindow = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a private chat exists for the provided user ID and performs necessary actions.
|
||||||
|
* @param {string} userID - The ID of the user.
|
||||||
|
*/
|
||||||
|
checkIfPrvChatExist(userID: string) {
|
||||||
|
const filterPrvChannelBoolean = this.channelService.allPrvChannels.some(
|
||||||
|
(chat) => chat.talkToUserId == userID
|
||||||
|
);
|
||||||
|
if (!filterPrvChannelBoolean) {
|
||||||
|
this.userService.createPrvChannel(userID);
|
||||||
|
} else {
|
||||||
|
const filterPrvChannelValue = this.channelService.allPrvChannels.filter(
|
||||||
|
(chat) => chat.talkToUserId == userID
|
||||||
|
);
|
||||||
|
this.chatService.getPrvChatId = filterPrvChannelValue[0].id!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters channels for a selected user.
|
||||||
|
* @param {string} currentChannel - The ID of the current channel.
|
||||||
|
* @returns {boolean} Boolean indicating if the channel is found.
|
||||||
|
*/
|
||||||
|
filterChannelForSelectedUser(currentChannel: string) {
|
||||||
|
const getBoolean = this.channelService.allChannels.some(
|
||||||
|
(channel) => channel.id == currentChannel
|
||||||
|
);
|
||||||
|
const getAddedUsers = this.channelService.allChannels.filter(
|
||||||
|
(channel) => channel.id == currentChannel
|
||||||
|
);
|
||||||
|
this.filterUsers(getAddedUsers[0].addedUser);
|
||||||
|
return getBoolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters users based on an array of user IDs.
|
||||||
|
* @param {string[]} userArray - The array of user IDs to filter.
|
||||||
|
*/
|
||||||
|
filterUsers(userArray: string[]) {
|
||||||
|
this.userService.getFiltertUsers = [];
|
||||||
|
for (let i = 0; i < this.userService.allUsers.length; i++) {
|
||||||
|
const currentUser = this.userService.allUsers[i];
|
||||||
|
if (userArray.includes(currentUser.id!)) {
|
||||||
|
this.userService.getFiltertUsers.push(currentUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the channel member window.
|
||||||
|
* @param {boolean} boolean - Boolean value to open or close the add member window.
|
||||||
|
*/
|
||||||
|
openMemberWindow(boolean: boolean) {
|
||||||
|
this.toggleBoolean.openChannelMemberWindow = true;
|
||||||
|
this.toggleBoolean.openAddMemberWindow(boolean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
<section>
|
||||||
|
<div class="darkBackground" (click)="closeWindow()"></div>
|
||||||
|
@if (showProfil) {
|
||||||
|
<!----- show prvProfil ----->
|
||||||
|
@for(i of talkToUser; track i){
|
||||||
|
<div class="whiteBox">
|
||||||
|
<div class="boxHeader">
|
||||||
|
<p>{{ 'open-send-prv-message.profil' | translate }}</p>
|
||||||
|
<div class="positionCloseIcon" (click)="closeWindow()">
|
||||||
|
<img src="./assets/img/closeIcon.svg" class="closeIcon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<img src="{{ i.avatar }}" class="profileImg" />
|
||||||
|
<div class="detailsBox">
|
||||||
|
<div class="editBtnPosition">
|
||||||
|
<p class="name">{{ i.firstName }} {{ i.lastName }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="statusBox">
|
||||||
|
<img
|
||||||
|
src="./assets/img/{{ i.status ? 'onlineRing.svg' : 'offlineRing.svg' }}"
|
||||||
|
class="onlineIcon"
|
||||||
|
/>
|
||||||
|
@if (i.status) {
|
||||||
|
<p class="fontColorGreen">{{ 'open-send-prv-message.aktiv' | translate }}</p>
|
||||||
|
} @else {
|
||||||
|
<p>{{ 'open-send-prv-message.offline' | translate }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="emailBox">
|
||||||
|
<img src="./assets/img/mail-icon.svg" />
|
||||||
|
<div class="email">
|
||||||
|
<p>{{ 'open-send-prv-message.adress' | translate }}</p>
|
||||||
|
<a
|
||||||
|
href="mailto:wwweewefeff@dede.de"
|
||||||
|
target="_blank"
|
||||||
|
class="fontNunito"
|
||||||
|
>{{ i.email }}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} } @else {
|
||||||
|
<!----- show channelMember ----->
|
||||||
|
@for(i of user; track i){
|
||||||
|
<div class="whiteBox">
|
||||||
|
<div class="boxHeader">
|
||||||
|
<p>{{ 'open-send-prv-message.profil' | translate }}</p>
|
||||||
|
<div class="positionCloseIcon" (click)="closeWindow()">
|
||||||
|
<img src="./assets/img/closeIcon.svg" class="closeIcon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<img src="{{ i.avatar }}" class="profileImg" />
|
||||||
|
<div class="detailsBox">
|
||||||
|
<div class="editBtnPosition">
|
||||||
|
<p class="name">{{ i.firstName }} {{ i.lastName }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="statusBox">
|
||||||
|
<img
|
||||||
|
src="./assets/img/{{ i.status ? 'onlineRing.svg' : 'offlineRing.svg' }}"
|
||||||
|
class="onlineIcon"
|
||||||
|
/>
|
||||||
|
@if (i.status) {
|
||||||
|
<p class="fontColorGreen">{{ 'open-send-prv-message.aktiv' | translate }}</p>
|
||||||
|
} @else {
|
||||||
|
<p>{{ 'open-send-prv-message.offline' | translate }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="emailBox">
|
||||||
|
<img src="./assets/img/mail-icon.svg" />
|
||||||
|
<div class="email">
|
||||||
|
<p>E-Mail-Address</p>
|
||||||
|
<a
|
||||||
|
href="mailto:wwweewefeff@dede.de"
|
||||||
|
target="_blank"
|
||||||
|
class="fontNunito"
|
||||||
|
>{{ i.email }}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button (click)="routeToUser(user)">
|
||||||
|
<img src="./assets/img/messageIcon.svg" /> {{ 'open-send-prv-message.message' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
} }
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
@import "../../../../../styles.scss";
|
||||||
|
|
||||||
|
.darkBackground {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whiteBox {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 440px;
|
||||||
|
height: auto;
|
||||||
|
background-color: whitesmoke;
|
||||||
|
border-radius: 35px;
|
||||||
|
z-index: 15;
|
||||||
|
@include displayFlex();
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 32px;
|
||||||
|
.boxHeader {
|
||||||
|
width: 360px;
|
||||||
|
@include displayFlex($j: space-between);
|
||||||
|
p {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.positionCloseIcon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
@include displayFlex();
|
||||||
|
margin-right: -10px;
|
||||||
|
transition: 0.3s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
background-color: #eceefe;
|
||||||
|
border-radius: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
> img {
|
||||||
|
filter: invert(59%) sepia(26%) saturate(871%) hue-rotate(199deg)
|
||||||
|
brightness(93%) contrast(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.profileImg {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 24px;
|
||||||
|
}
|
||||||
|
> button {
|
||||||
|
@include displayFlex($g: 12px);
|
||||||
|
background-color: #676eee;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 12px;
|
||||||
|
padding-left: 22px;
|
||||||
|
padding-right: 22px;
|
||||||
|
font-size: larger;
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
margin: 32px;
|
||||||
|
margin-top: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.3s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
background-color: #444df2;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.1) 0px 20px 25px -5px,
|
||||||
|
rgba(0, 0, 0, 0.04) 0px 10px 10px -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsBox {
|
||||||
|
width: 360px;
|
||||||
|
.editBtnPosition {
|
||||||
|
@include displayFlex($j: space-between);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
.name {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.editBtn {
|
||||||
|
color: #535af1;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.statusBox {
|
||||||
|
@include displayFlex($j: flex-start, $g: 12px);
|
||||||
|
margin-bottom: 40px;
|
||||||
|
> p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: medium;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fontColorGreen {
|
||||||
|
color: #92c83e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailBox {
|
||||||
|
@include displayFlex($j: flex-start, $a: flex-start, $g: 20px);
|
||||||
|
height: 96px;
|
||||||
|
margin-top: 24px;
|
||||||
|
img {
|
||||||
|
width: 25px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
.email {
|
||||||
|
> p {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
> a {
|
||||||
|
color: #4ea1ff;
|
||||||
|
font-size: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background: linear-gradient(currentColor 0 0) bottom left/
|
||||||
|
var(--underline-width, 0%) 0.1em no-repeat;
|
||||||
|
display: inline-block;
|
||||||
|
transition: background-size 0.5s;
|
||||||
|
&:hover {
|
||||||
|
--underline-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------- RESPONSIVE -------------*/
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE) {
|
||||||
|
.whiteBox {
|
||||||
|
width: 280px;
|
||||||
|
padding-top: 16px;
|
||||||
|
.boxHeader {
|
||||||
|
width: 230px;
|
||||||
|
p {
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.profileImg {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.detailsBox {
|
||||||
|
width: 230px;
|
||||||
|
.editBtnPosition {
|
||||||
|
.name {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailBox {
|
||||||
|
@include displayFlex($j: flex-start, $a: flex-start, $g: 10px);
|
||||||
|
img {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.email {
|
||||||
|
> p {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
> a {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { OpenSendPrvMessageWindowComponent } from './open-send-prv-message-window.component';
|
||||||
|
|
||||||
|
describe('OpenSendPrvMessageWindowComponent', () => {
|
||||||
|
let component: OpenSendPrvMessageWindowComponent;
|
||||||
|
let fixture: ComponentFixture<OpenSendPrvMessageWindowComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [OpenSendPrvMessageWindowComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(OpenSendPrvMessageWindowComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { User } from '../../../../interface/user.interface';
|
||||||
|
import { ToggleBooleanService } from '../../../../service/toggle-boolean.service';
|
||||||
|
import { ChannleService } from '../../../../service/channle.service';
|
||||||
|
import { UserService } from '../../../../service/user.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-open-send-prv-message-window',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, TranslateModule],
|
||||||
|
templateUrl: './open-send-prv-message-window.component.html',
|
||||||
|
styleUrl: './open-send-prv-message-window.component.scss',
|
||||||
|
})
|
||||||
|
export class OpenSendPrvMessageWindowComponent {
|
||||||
|
@Input() user!: User[];
|
||||||
|
@Input() talkToUser!: User[];
|
||||||
|
@Input() openUserWindowBoolean!: boolean;
|
||||||
|
@Input() showProfil!: boolean;
|
||||||
|
@Output() closeUserWondow = new EventEmitter<boolean>();
|
||||||
|
@Output() showProfilWindow = new EventEmitter<boolean>();
|
||||||
|
isOnline: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public toggleBoolean: ToggleBooleanService,
|
||||||
|
private channelService: ChannleService,
|
||||||
|
public userService: UserService,
|
||||||
|
private route: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the user window and profile window if open.
|
||||||
|
* Emits events to notify about window closure.
|
||||||
|
*/
|
||||||
|
closeWindow() {
|
||||||
|
this.openUserWindowBoolean = false;
|
||||||
|
this.showProfil = false;
|
||||||
|
this.closeUserWondow.emit(this.openUserWindowBoolean);
|
||||||
|
this.showProfilWindow.emit(this.showProfil);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes all open windows and resets toggle booleans.
|
||||||
|
* Emits events to notify about window closures.
|
||||||
|
*/
|
||||||
|
closeEverything() {
|
||||||
|
this.openUserWindowBoolean = false;
|
||||||
|
this.closeUserWondow.emit(this.openUserWindowBoolean);
|
||||||
|
this.toggleBoolean.openChannelMemberWindow = false;
|
||||||
|
this.toggleBoolean.closeChannelMemberWindow = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routes to the user's private chat.
|
||||||
|
* If the chat channel does not exist, it creates one.
|
||||||
|
* @param {User[]} user - The user to route to.
|
||||||
|
*/
|
||||||
|
routeToUser(user: User[]) {
|
||||||
|
const userId = user[0].id!;
|
||||||
|
const channelExistsBoolean = this.channelService.allPrvChannels.some(
|
||||||
|
(channel) =>
|
||||||
|
(channel.creatorId === userId &&
|
||||||
|
channel.talkToUserId === this.userService.getCurrentUserId()) ||
|
||||||
|
(channel.creatorId === this.userService.getCurrentUserId() &&
|
||||||
|
channel.talkToUserId === userId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!channelExistsBoolean) {
|
||||||
|
const createChannelPromise = this.userService.createPrvChannel(userId);
|
||||||
|
if (createChannelPromise instanceof Promise) {
|
||||||
|
createChannelPromise.then((docId) => {
|
||||||
|
this.route.navigateByUrl(`main/${docId}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.getRouteToPrvChat(userId, channelExistsBoolean);
|
||||||
|
}
|
||||||
|
this.closeEverything();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routes to the existing private chat.
|
||||||
|
* @param {string} userId - The ID of the user to route to.
|
||||||
|
* @param {boolean} channelExistsBoolean - Indicates whether the chat channel exists.
|
||||||
|
*/
|
||||||
|
getRouteToPrvChat(userId: string, channelExistsBoolean: boolean) {
|
||||||
|
if (channelExistsBoolean) {
|
||||||
|
const existingChannel = this.channelService.allPrvChannels.find(
|
||||||
|
(channel) =>
|
||||||
|
(channel.creatorId === userId &&
|
||||||
|
channel.talkToUserId === this.userService.getCurrentUserId()) ||
|
||||||
|
(channel.creatorId === this.userService.getCurrentUserId() &&
|
||||||
|
channel.talkToUserId === userId)
|
||||||
|
);
|
||||||
|
this.route.navigateByUrl(`main/${existingChannel!.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
<section>
|
||||||
|
<div class="darkBackground" (click)="closeChannelMemberWindow()"></div>
|
||||||
|
@if (!toggleBoolean.closeChannelMemberWindow) {
|
||||||
|
<div
|
||||||
|
class="whiteBox"
|
||||||
|
[ngStyle]="{
|
||||||
|
right: isSecondaryChatOpen ? '620px' : ''
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="posiotionHeader">
|
||||||
|
<p>{{ "show-channel-member.header" | translate }}</p>
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'closeIcon.svg'"
|
||||||
|
[imgSize]="'16px'"
|
||||||
|
[btnSize]="'32px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
(click)="closeChannelMemberWindow()"
|
||||||
|
style="margin-right: -12px"
|
||||||
|
></app-small-btn>
|
||||||
|
</div>
|
||||||
|
<div class="positionOfAllUsersInBox">
|
||||||
|
@for(user of getFiltertUsers; track user){
|
||||||
|
<div class="user" (click)="openUserWindow(user)">
|
||||||
|
<div class="positionImgs">
|
||||||
|
<img src="{{ user.avatar }}" class="avatarImg" />
|
||||||
|
<img
|
||||||
|
src="./assets/img/{{
|
||||||
|
user.status ? 'onlineRing.svg' : 'offlineRing.svg'
|
||||||
|
}}"
|
||||||
|
class="onlineIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1>{{ user.firstName }} {{ user.lastName }}</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="addMemberBox" (click)="toggleBoolean.openAddMemberWindow(true)">
|
||||||
|
<img src="./assets/img/addPerson.svg" />
|
||||||
|
<p>{{ "show-channel-member.addMember" | translate }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div
|
||||||
|
class="addMemberToChannelBox"
|
||||||
|
[ngStyle]="{
|
||||||
|
right: isSecondaryChatOpen ? '580px' : ''
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="posiotionHeader">
|
||||||
|
<div class="headline">
|
||||||
|
<p>{{ "show-channel-member.addMember" | translate }}</p>
|
||||||
|
@if (getChannelName(currentChannel)) {
|
||||||
|
<span># {{ getCurrentChannelName }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'closeIcon.svg'"
|
||||||
|
[imgSize]="'16px'"
|
||||||
|
[btnSize]="'32px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
(click)="closeChannelMemberWindow()"
|
||||||
|
style="margin-right: -12px"
|
||||||
|
></app-small-btn>
|
||||||
|
</div>
|
||||||
|
<div class="addedUserBox">
|
||||||
|
<div
|
||||||
|
class="userPosition"
|
||||||
|
*ngFor="let user of getSelectedUsers; let i = index"
|
||||||
|
>
|
||||||
|
<img src="{{ user.avatar }}" class="avatarImg" />
|
||||||
|
<p>{{ user.firstName }} {{ user.lastName }}</p>
|
||||||
|
<img
|
||||||
|
src="./assets/img/closeIcon.svg"
|
||||||
|
class="imgClose"
|
||||||
|
(click)="spliceCurrentUser(i)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="showSearchedUsersWindow"
|
||||||
|
[ngClass]="{ dBlock: showExistenUsers, dNone: !showExistenUsers }"
|
||||||
|
>
|
||||||
|
<div class="positionOfAllUsersBox">
|
||||||
|
@if(getSearchedUser.length <= 0) {
|
||||||
|
<span>{{ "show-channel-member.result" | translate }}</span> }
|
||||||
|
@for(user of getSearchedUser; track user){
|
||||||
|
@if(!isUserAlreadySelectet(user)) {
|
||||||
|
<div class="userBox" (click)="chooseUser(user)">
|
||||||
|
<div class="positionImgsBox">
|
||||||
|
<img src="{{ user.avatar }}" class="addAvatarImg" />
|
||||||
|
<img
|
||||||
|
src="./assets/img/{{
|
||||||
|
user.status ? 'onlineRing.svg' : 'offlineRing.svg'
|
||||||
|
}}"
|
||||||
|
class="onlineIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>{{ user.firstName }} {{ user.lastName }}</p>
|
||||||
|
</div>
|
||||||
|
} }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="inputfieldStyle"
|
||||||
|
[(ngModel)]="userName"
|
||||||
|
(keyup)="filterUsers(userName)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="positionCreateBtn">
|
||||||
|
<button
|
||||||
|
class="furterBtnClass"
|
||||||
|
[ngClass]="{
|
||||||
|
furterBtnClass: getSelectedUsers.length > 0,
|
||||||
|
disabledBtn: getSelectedUsers.length === 0
|
||||||
|
}"
|
||||||
|
[disabled]="getSelectedUsers.length === 0"
|
||||||
|
(click)="addUserToChannel()"
|
||||||
|
>
|
||||||
|
{{ "show-channel-member.add" | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
<app-open-send-prv-message-window
|
||||||
|
[ngClass]="{ dBlock: openUserWindowBoolean, dNone: !openUserWindowBoolean }"
|
||||||
|
[user]="user"
|
||||||
|
[openUserWindowBoolean]="openUserWindowBoolean"
|
||||||
|
(closeUserWondow)="changeOpenUserWindowBoolean($event)"
|
||||||
|
></app-open-send-prv-message-window>
|
||||||
|
|
@ -0,0 +1,344 @@
|
||||||
|
@import "../../../../styles.scss";
|
||||||
|
|
||||||
|
.darkBackground {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whiteBox {
|
||||||
|
position: absolute;
|
||||||
|
top: 199px;
|
||||||
|
right: 108px;
|
||||||
|
width: 320px;
|
||||||
|
height: fit-content;
|
||||||
|
z-index: 4;
|
||||||
|
border-radius: 35px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
background-color: whitesmoke;
|
||||||
|
padding: 24px;
|
||||||
|
@include displayFlex($g: 20px);
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.posiotionHeader {
|
||||||
|
@include displayFlex($j: space-between);
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.positionOfAllUsersInBox {
|
||||||
|
@include displayFlex($j: flex-start, $a: flex-start);
|
||||||
|
flex-direction: column;
|
||||||
|
height: fit-content;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.user {
|
||||||
|
@include displayFlex($g: 12px);
|
||||||
|
padding: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #edeefe;
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positionImgs {
|
||||||
|
@include displayFlex($a: flex-end);
|
||||||
|
|
||||||
|
.avatarImg {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onlineIcon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addMemberBox {
|
||||||
|
@include displayFlex($g: 20px);
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #888dec;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addMemberToChannelBox {
|
||||||
|
position: absolute;
|
||||||
|
top: 198px;
|
||||||
|
right: 62px;
|
||||||
|
width: 400px;
|
||||||
|
padding: 24px;
|
||||||
|
z-index: 3;
|
||||||
|
height: auto;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 35px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
@include displayFlex($g: 20px);
|
||||||
|
flex-direction: column;
|
||||||
|
.posiotionHeader {
|
||||||
|
@include displayFlex($j: space-between, $a: flex-start);
|
||||||
|
width: 100%;
|
||||||
|
.headline {
|
||||||
|
@include displayFlex($a: flex-start, $g: 12px);
|
||||||
|
flex-direction: column;
|
||||||
|
p {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #797ef3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addedUserBox {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
z-index: 4;
|
||||||
|
height: auto;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #888dec;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 25px;
|
||||||
|
transition: 0.3s ease-in-out;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: 1px solid #2c36f0;
|
||||||
|
}
|
||||||
|
.userPosition {
|
||||||
|
@include displayFlex($g: 8px);
|
||||||
|
padding: 4px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
background-color: #edeefe;
|
||||||
|
border-radius: 25px;
|
||||||
|
border-radius: 25px;
|
||||||
|
margin: 4px;
|
||||||
|
p {
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
.avatarImg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imgClose {
|
||||||
|
width: 12px;
|
||||||
|
object-fit: contain;
|
||||||
|
transition: 0.1s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
scale: 1.1;
|
||||||
|
filter: brightness(0) saturate(100%) invert(8%) sepia(96%)
|
||||||
|
saturate(6339%) hue-rotate(1deg) brightness(102%) contrast(109%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputfieldStyle {
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
height: 26px;
|
||||||
|
padding: 12px;
|
||||||
|
height: auto;
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.showSearchedUsersWindow::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showSearchedUsersWindow {
|
||||||
|
position: absolute;
|
||||||
|
top: 88%;
|
||||||
|
left: 20px;
|
||||||
|
z-index: 3;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid #888dec;
|
||||||
|
border-radius: 25px;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
background-color: white;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 210px;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 240px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
text-align: start;
|
||||||
|
.positionOfAllUsersBox {
|
||||||
|
@include displayFlex($a: flex-start);
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
span {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
.userBox {
|
||||||
|
@include displayFlex($g: 12px);
|
||||||
|
padding: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
p {
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: #edeefe;
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positionImgsBox {
|
||||||
|
@include displayFlex($a: flex-end);
|
||||||
|
|
||||||
|
.addAvatarImg {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onlineIcon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.positionCreateBtn {
|
||||||
|
width: 100%;
|
||||||
|
@include displayFlex($j: flex-end);
|
||||||
|
}
|
||||||
|
|
||||||
|
.furterBtnClass {
|
||||||
|
width: 120px;
|
||||||
|
height: 48px;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #888dec;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: larger;
|
||||||
|
color: #444df2;
|
||||||
|
transition: 0.3s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
background-color: #444df2;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabledBtn {
|
||||||
|
background-color: #22222242;
|
||||||
|
border: none;
|
||||||
|
font-size: larger;
|
||||||
|
color: #fff;
|
||||||
|
width: 120px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: larger;
|
||||||
|
transition: 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------- RESPONSIVE -------------*/
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE) {
|
||||||
|
.whiteBox {
|
||||||
|
top: 140px;
|
||||||
|
right: 20px;
|
||||||
|
width: 250px;
|
||||||
|
.posiotionHeader {
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addMemberToChannelBox {
|
||||||
|
position: absolute;
|
||||||
|
top: 140px;
|
||||||
|
right: 20px;
|
||||||
|
width: 230px;
|
||||||
|
.posiotionHeader {
|
||||||
|
.headline {
|
||||||
|
p {
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.showSearchedUsersWindow {
|
||||||
|
position: absolute;
|
||||||
|
top: 88%;
|
||||||
|
right: 0px;
|
||||||
|
max-height: 210px;
|
||||||
|
width: 200px;
|
||||||
|
.positionOfAllUsersBox {
|
||||||
|
span {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.userBox {
|
||||||
|
p {
|
||||||
|
font-size: 18px;
|
||||||
|
font-style: 600;
|
||||||
|
}
|
||||||
|
.positionImgsBox {
|
||||||
|
.addAvatarImg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
.onlineIcon {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
margin-left: -8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-height: 600px) {
|
||||||
|
.positionOfAllUsersInBox {
|
||||||
|
max-height: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-height: 500px) {
|
||||||
|
.positionOfAllUsersInBox {
|
||||||
|
max-height: 170px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ShowChannelMemberComponent } from './show-channel-member.component';
|
||||||
|
|
||||||
|
describe('ShowChannelMemberComponent', () => {
|
||||||
|
let component: ShowChannelMemberComponent;
|
||||||
|
let fixture: ComponentFixture<ShowChannelMemberComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ShowChannelMemberComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ShowChannelMemberComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { ToggleBooleanService } from '../../../service/toggle-boolean.service';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SmallBtnComponent } from '../../../shared/components/small-btn/small-btn.component';
|
||||||
|
import { User } from '../../../interface/user.interface';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { ChannleService } from '../../../service/channle.service';
|
||||||
|
import { UserService } from '../../../service/user.service';
|
||||||
|
import { OpenSendPrvMessageWindowComponent } from './open-send-prv-message-window/open-send-prv-message-window.component';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-show-channel-member',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SmallBtnComponent,
|
||||||
|
FormsModule,
|
||||||
|
OpenSendPrvMessageWindowComponent,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
templateUrl: './show-channel-member.component.html',
|
||||||
|
styleUrl: './show-channel-member.component.scss',
|
||||||
|
})
|
||||||
|
export class ShowChannelMemberComponent {
|
||||||
|
@Input() isSecondaryChatOpen: boolean = false;
|
||||||
|
|
||||||
|
userName: string = '';
|
||||||
|
showExistenUsers: boolean = false;
|
||||||
|
getSearchedUser: User[] = [];
|
||||||
|
getCurrentChannelName: string = '';
|
||||||
|
getSelectedUsers: User[] = [];
|
||||||
|
selectedUsers: string[] = [];
|
||||||
|
|
||||||
|
openUserWindowBoolean: boolean = false;
|
||||||
|
user: User[] = [];
|
||||||
|
|
||||||
|
@Input() getFiltertUsers!: User[];
|
||||||
|
@Input() currentChannel!: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public toggleBoolean: ToggleBooleanService,
|
||||||
|
public channelService: ChannleService,
|
||||||
|
public userService: UserService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the channel member window and resets related values.
|
||||||
|
*/
|
||||||
|
closeChannelMemberWindow() {
|
||||||
|
this.toggleBoolean.openChannelMemberWindow = false;
|
||||||
|
this.toggleBoolean.closeChannelMemberWindow = false;
|
||||||
|
this.resetValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters users based on search value and updates the list of searched users.
|
||||||
|
* @param {string} searchValue - The value to search for.
|
||||||
|
*/
|
||||||
|
filterUsers(searchValue: string) {
|
||||||
|
if (searchValue != '') {
|
||||||
|
this.showExistenUsers = true;
|
||||||
|
this.getSearchedUser = [];
|
||||||
|
const searchedUser = searchValue.toLowerCase().trim();
|
||||||
|
const filteredUsers = this.userService.getUsers().filter((user) => {
|
||||||
|
const fullName = `${user.firstName} ${user.lastName}`.toLowerCase();
|
||||||
|
return fullName.includes(searchedUser);
|
||||||
|
});
|
||||||
|
this.checkIfUserIsInChannel(filteredUsers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if users from the filtered list are already in the current channel and updates the searched user list.
|
||||||
|
* @param {User[]} filteredUsers - The list of filtered users.
|
||||||
|
*/
|
||||||
|
checkIfUserIsInChannel(filteredUsers: User[]) {
|
||||||
|
const getChannel = this.channelService.allChannels.filter(
|
||||||
|
(channel) => channel.id === this.currentChannel
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const user of getChannel) {
|
||||||
|
const userArray = user.addedUser;
|
||||||
|
this.channelService.channelMembers = userArray;
|
||||||
|
|
||||||
|
for (const user of filteredUsers) {
|
||||||
|
const isUserInChannel = userArray.some(
|
||||||
|
(channelUser) => channelUser === user.id
|
||||||
|
);
|
||||||
|
if (!isUserInChannel) {
|
||||||
|
this.getSearchedUser.push(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a user is already selected.
|
||||||
|
* @param {User} user - The user to check.
|
||||||
|
* @returns {boolean} Returns true if the user is already selected, otherwise false.
|
||||||
|
*/
|
||||||
|
isUserAlreadySelectet(user: User) {
|
||||||
|
return this.getSelectedUsers.some(
|
||||||
|
(selectedUser) => selectedUser.id === user.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a user to the list of selected users.
|
||||||
|
* @param {User} user - The user to add.
|
||||||
|
*/
|
||||||
|
chooseUser(user: User) {
|
||||||
|
const isUserAlreadySelected = this.getSelectedUsers.some(
|
||||||
|
(selectedUser) => selectedUser.id === user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUserAlreadySelected) {
|
||||||
|
this.selectedUsers.push(user.id!);
|
||||||
|
this.getSelectedUsers.push(user);
|
||||||
|
}
|
||||||
|
this.userName = '';
|
||||||
|
this.showExistenUsers = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the user at the specified index from the list of selected users.
|
||||||
|
* @param {number} index - The index of the user to remove.
|
||||||
|
*/
|
||||||
|
spliceCurrentUser(index: number) {
|
||||||
|
this.getSelectedUsers.splice(index, 1);
|
||||||
|
this.showExistenUsers = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the current channel.
|
||||||
|
* @param {string} currentChannel - The ID of the current channel.
|
||||||
|
* @returns {boolean} Returns true if the channel exists, otherwise false.
|
||||||
|
*/
|
||||||
|
getChannelName(currentChannel: string) {
|
||||||
|
const getName = this.channelService.allChannels.some(
|
||||||
|
(channel) => channel.id == currentChannel
|
||||||
|
);
|
||||||
|
const getChannelName = this.channelService.allChannels.filter(
|
||||||
|
(channel) => channel.id == currentChannel
|
||||||
|
);
|
||||||
|
this.getCurrentChannelName = getChannelName[0].name;
|
||||||
|
return getName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds selected users to the current channel.
|
||||||
|
*/
|
||||||
|
addUserToChannel() {
|
||||||
|
this.channelService.addNewMemberToChannel(
|
||||||
|
'channels',
|
||||||
|
this.currentChannel,
|
||||||
|
this.selectedUsers,
|
||||||
|
'addUserToChannel'
|
||||||
|
);
|
||||||
|
this.closeChannelMemberWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a user window.
|
||||||
|
*
|
||||||
|
* @param {User} user - The user to be displayed in the window.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
openUserWindow(user: User) {
|
||||||
|
this.user = [user];
|
||||||
|
this.openUserWindowBoolean = !this.openUserWindowBoolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens or closes the user window based on the given value.
|
||||||
|
* @param {boolean} value - The value to set for opening/closing the user window.
|
||||||
|
*/
|
||||||
|
changeOpenUserWindowBoolean(value: boolean) {
|
||||||
|
this.openUserWindowBoolean = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets various values used in the component.
|
||||||
|
*/
|
||||||
|
resetValues() {
|
||||||
|
this.userName = '';
|
||||||
|
this.showExistenUsers = false;
|
||||||
|
this.getSearchedUser = [];
|
||||||
|
this.getCurrentChannelName = '';
|
||||||
|
this.getSelectedUsers = [];
|
||||||
|
this.selectedUsers = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
<ng-container *ngIf="downloadFilesService.downloadedFiles | async as files">
|
||||||
|
<div class="attachments" *ngFor="let folder of files">
|
||||||
|
<div *ngIf="chatId == folder.id">
|
||||||
|
<br />
|
||||||
|
<div class="files">
|
||||||
|
<div *ngFor="let file of folder.files">
|
||||||
|
<div class="filesView">
|
||||||
|
@if (['png', 'jpg', 'jpeg', 'gif'].includes(getFileType(file))) {
|
||||||
|
<img
|
||||||
|
src="{{ file }}"
|
||||||
|
alt="image"
|
||||||
|
loading="lazy"
|
||||||
|
[ngStyle]="{
|
||||||
|
height:
|
||||||
|
openOnSecondaryChat && viewWidth >= RESPONSIVE_THRESHOLD
|
||||||
|
? '5vw'
|
||||||
|
: '7.5vw'
|
||||||
|
}"
|
||||||
|
(click)="showOverlay(file)"
|
||||||
|
/>
|
||||||
|
} @if (['pdf', 'doc', 'txt'].includes(getFileType(file))) {
|
||||||
|
<a href="{{ file }}" target="_blank">
|
||||||
|
<img
|
||||||
|
class="otherFiles"
|
||||||
|
src="./assets/img/attachments/{{ getFileType(file) }}.png"
|
||||||
|
alt="image"
|
||||||
|
loading="lazy"
|
||||||
|
[ngStyle]="{
|
||||||
|
height:
|
||||||
|
openOnSecondaryChat && viewWidth >= RESPONSIVE_THRESHOLD
|
||||||
|
? '3vw'
|
||||||
|
: '5vw'
|
||||||
|
}"
|
||||||
|
/></a>
|
||||||
|
} @if (['mp3', 'wav', 'wma'].includes(getFileType(file))) {
|
||||||
|
<audio
|
||||||
|
controls
|
||||||
|
preload="auto"
|
||||||
|
src="{{ file }}"
|
||||||
|
loading="lazy"
|
||||||
|
[ngStyle]="{
|
||||||
|
width:
|
||||||
|
openOnSecondaryChat && viewWidth >= RESPONSIVE_THRESHOLD
|
||||||
|
? '250px'
|
||||||
|
: ''
|
||||||
|
}"
|
||||||
|
></audio>
|
||||||
|
}@if (getFileType(file) === 'mp4') {
|
||||||
|
<div (click)="showOverlay(file)">
|
||||||
|
<video
|
||||||
|
src="{{ file }}"
|
||||||
|
alt="video"
|
||||||
|
loading="lazy"
|
||||||
|
[ngStyle]="{
|
||||||
|
height:
|
||||||
|
openOnSecondaryChat && viewWidth >= RESPONSIVE_THRESHOLD
|
||||||
|
? '5vw'
|
||||||
|
: '7.5vw'
|
||||||
|
}"
|
||||||
|
></video>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
@import "./../../../../../styles.scss";
|
||||||
|
|
||||||
|
.files {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
max-width: calc(100vw - 140px);
|
||||||
|
overflow: auto;
|
||||||
|
.filesView {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.3s ease-in-out;
|
||||||
|
filter: drop-shadow(3px 3px 3px rgba(0, 0, 0, 0.3));
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
height: 7.5vw;
|
||||||
|
min-height: 80px;
|
||||||
|
width: auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.otherFiles {
|
||||||
|
height: 5vw;
|
||||||
|
min-height: 60px;
|
||||||
|
width: auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
filter: drop-shadow(5px 5px 5px rgba(0, 0, 0, 0.35));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------- RESPONSIVE -------------*/
|
||||||
|
|
||||||
|
@media screen and (max-width: 510px) {
|
||||||
|
.files {
|
||||||
|
.filesView {
|
||||||
|
audio {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 460px) {
|
||||||
|
.files {
|
||||||
|
.filesView {
|
||||||
|
audio {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 410px) {
|
||||||
|
.files {
|
||||||
|
.filesView {
|
||||||
|
audio {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 360px) {
|
||||||
|
.files {
|
||||||
|
.filesView {
|
||||||
|
audio {
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AttachmentsComponent } from './attachments.component';
|
||||||
|
|
||||||
|
describe('AttachmentsComponent', () => {
|
||||||
|
let component: AttachmentsComponent;
|
||||||
|
let fixture: ComponentFixture<AttachmentsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AttachmentsComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AttachmentsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { DownloadFilesService } from '../../../../service/download-files.service';
|
||||||
|
import { OverlayService } from '../../../../service/overlay.service';
|
||||||
|
import { OverlayComponent } from '../../../../shared/components/overlay/overlay.component';
|
||||||
|
import { SharedService } from '../../../../service/shared.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-attachments',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, OverlayComponent],
|
||||||
|
templateUrl: './attachments.component.html',
|
||||||
|
styleUrl: './attachments.component.scss',
|
||||||
|
})
|
||||||
|
export class AttachmentsComponent {
|
||||||
|
@Input() chatId: string = '';
|
||||||
|
@Input() openOnSecondaryChat: boolean = false;
|
||||||
|
@Input() viewWidth: number = 0;
|
||||||
|
imageUrl: string = '';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public downloadFilesService: DownloadFilesService,
|
||||||
|
private overlayService: OverlayService,
|
||||||
|
private sharedService: SharedService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
RESPONSIVE_THRESHOLD = this.sharedService.RESPONSIVE_THRESHOLD;
|
||||||
|
|
||||||
|
// Type of files:
|
||||||
|
// Img: PNG, GIF, JPG, JPEG
|
||||||
|
// Files: PDF, DOC, TXT
|
||||||
|
// Audio: MP3, WMA, WAV
|
||||||
|
// Video: MP4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the file type from the given file name.
|
||||||
|
* @param {string} file - The name of the file.
|
||||||
|
* @returns {string} The file type extracted from the file name.
|
||||||
|
*/
|
||||||
|
getFileType(file: string): string {
|
||||||
|
const extension = file.split('.').pop()?.toLowerCase();
|
||||||
|
const getTag = extension!.split('?')[0];
|
||||||
|
return getTag || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows an overlay using the provided file name.
|
||||||
|
* @param {string} file - The name of the file to display in the overlay.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
showOverlay(file: string) {
|
||||||
|
this.overlayService.setOverlayData(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
<form
|
||||||
|
(ngSubmit)="onSubmit(chat.id, taskForm)"
|
||||||
|
#taskForm="ngForm"
|
||||||
|
onsubmit="return false"
|
||||||
|
(mouseleave)="this.isEmojiPickerVisible = false"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
type="text"
|
||||||
|
id="message"
|
||||||
|
name="message"
|
||||||
|
minlength="6"
|
||||||
|
#message="ngModel"
|
||||||
|
[(ngModel)]="chat.message"
|
||||||
|
(keyup)="sendMessageWithEnter($event, chat.id, taskForm)"
|
||||||
|
>{{ originalMessage }}</textarea
|
||||||
|
>
|
||||||
|
|
||||||
|
@if(isEmojiPickerVisible) {
|
||||||
|
<app-emoji-picker
|
||||||
|
[output]="'native'"
|
||||||
|
(emojiOutputEmitter)="emojiOutputEmitter($event)"
|
||||||
|
></app-emoji-picker>
|
||||||
|
}
|
||||||
|
<div class="footer">
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'add-reaction.svg'"
|
||||||
|
[imgSize]="'24px'"
|
||||||
|
[btnSize]="'36px'"
|
||||||
|
[imgSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '24px' : '17px'"
|
||||||
|
[btnSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '36px' : '25px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
(click)="toggleEmojiPicker()"
|
||||||
|
></app-small-btn>
|
||||||
|
<div class="btns">
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
class="btn"
|
||||||
|
[ngStyle]="{
|
||||||
|
'font-size':
|
||||||
|
viewWidth >= RESPONSIVE_THRESHOLD && openOnSecondaryChat
|
||||||
|
? '14px'
|
||||||
|
: ''
|
||||||
|
}"
|
||||||
|
(click)="closeEditMsg()"
|
||||||
|
>
|
||||||
|
{{ "edit-msg.cancel" | translate }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn"
|
||||||
|
[ngStyle]="{
|
||||||
|
'font-size':
|
||||||
|
viewWidth >= RESPONSIVE_THRESHOLD && openOnSecondaryChat
|
||||||
|
? '14px'
|
||||||
|
: ''
|
||||||
|
}"
|
||||||
|
[disabled]="message.invalid || chat.message == originalMessage"
|
||||||
|
>
|
||||||
|
{{ "edit-msg.save" | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
@import "../../../../../styles.scss";
|
||||||
|
|
||||||
|
form {
|
||||||
|
position: relative;
|
||||||
|
width: calc(100% - 24px);
|
||||||
|
textarea {
|
||||||
|
width: calc(100% - 6px);
|
||||||
|
height: 100px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 12px;
|
||||||
|
resize: none;
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
&:invalid {
|
||||||
|
border: 1px solid red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app-emoji-picker {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 50px;
|
||||||
|
left: 20px;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-small-btn {
|
||||||
|
padding: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btns {
|
||||||
|
padding: 12px 0;
|
||||||
|
.btn {
|
||||||
|
padding: 9px 15px;
|
||||||
|
border-radius: 50px;
|
||||||
|
border: 1px solid #6971f5;
|
||||||
|
background-color: #6971f5;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
&:disabled {
|
||||||
|
background-color: #686868;
|
||||||
|
}
|
||||||
|
&:first-child {
|
||||||
|
margin-right: 12px;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #6971f5;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: #444df2;
|
||||||
|
border-color: #444df2;
|
||||||
|
color: #fff;
|
||||||
|
&:disabled {
|
||||||
|
background-color: #686868;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.interactIcons {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
padding: 8px;
|
||||||
|
&:hover {
|
||||||
|
scale: 1.1;
|
||||||
|
background-color: #b7bae7;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: brightness(120%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabledIcon {
|
||||||
|
filter: brightness(0) saturate(100%) invert(45%) sepia(75%) saturate(684%)
|
||||||
|
hue-rotate(204deg) brightness(101%) contrast(91%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.files {
|
||||||
|
position: absolute;
|
||||||
|
padding-top: 12px;
|
||||||
|
top: 0;
|
||||||
|
right: -24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileBox {
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
background-color: #eee;
|
||||||
|
max-width: 40px;
|
||||||
|
max-height: 20px;
|
||||||
|
margin-right: 12px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
.fileIcons {
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-right: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0) saturate(100%) invert(20%) sepia(40%)
|
||||||
|
saturate(6312%) hue-rotate(236deg) brightness(106%) contrast(90%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
right: -4px;
|
||||||
|
width: 16px;
|
||||||
|
object-fit: contain;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0) saturate(100%) invert(20%) sepia(78%) saturate(5090%)
|
||||||
|
hue-rotate(5deg) brightness(105%) contrast(103%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE) {
|
||||||
|
.btns {
|
||||||
|
padding: 6px 0;
|
||||||
|
.btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
&:first-child {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app-small-btn {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
width: calc(100% - 12px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 360px) {
|
||||||
|
.btns {
|
||||||
|
.btn {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { EditMsgComponent } from './edit-msg.component';
|
||||||
|
|
||||||
|
describe('EditMsgComponent', () => {
|
||||||
|
let component: EditMsgComponent;
|
||||||
|
let fixture: ComponentFixture<EditMsgComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [EditMsgComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(EditMsgComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
HostListener,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
Chat,
|
||||||
|
ChatAnswers,
|
||||||
|
ChatReactions,
|
||||||
|
} from '../../../../interface/chat.interface';
|
||||||
|
import { SingleChatComponent } from '../single-chat.component';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule, NgForm } from '@angular/forms';
|
||||||
|
import { ChatService } from '../../../../service/chat.service';
|
||||||
|
import { PickerComponent } from '@ctrl/ngx-emoji-mart';
|
||||||
|
import { SmallBtnComponent } from '../../../../shared/components/small-btn/small-btn.component';
|
||||||
|
import { DownloadFilesService } from '../../../../service/download-files.service';
|
||||||
|
import { EmojiPickerComponent } from '../../../../shared/components/emoji-picker/emoji-picker.component';
|
||||||
|
import { UserService } from '../../../../service/user.service';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { SharedService } from '../../../../service/shared.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-edit-msg',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
SingleChatComponent,
|
||||||
|
PickerComponent,
|
||||||
|
SmallBtnComponent,
|
||||||
|
EmojiPickerComponent,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
templateUrl: './edit-msg.component.html',
|
||||||
|
styleUrl: './edit-msg.component.scss',
|
||||||
|
})
|
||||||
|
export class EditMsgComponent {
|
||||||
|
@Input() chat!: Chat | ChatAnswers;
|
||||||
|
@Input() viewWidth: number = 0;
|
||||||
|
@Input() openOnSecondaryChat: boolean = false;
|
||||||
|
@Output() closeEditMsgEmitter: EventEmitter<boolean> =
|
||||||
|
new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
isEmojiPickerVisible: boolean | undefined;
|
||||||
|
public originalMessage: string = '';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public chatService: ChatService,
|
||||||
|
public downloadFilesService: DownloadFilesService,
|
||||||
|
public userService: UserService,
|
||||||
|
private sharedService: SharedService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
RESPONSIVE_THRESHOLD = this.sharedService.RESPONSIVE_THRESHOLD;
|
||||||
|
RESPONSIVE_THRESHOLD_MOBILE = this.sharedService.RESPONSIVE_THRESHOLD_MOBILE;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.originalMessage = this.chat.message as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the original message and emit an event to close the edit message form.
|
||||||
|
*/
|
||||||
|
closeEditMsg() {
|
||||||
|
this.chat.message = this.originalMessage;
|
||||||
|
this.closeEditMsgEmitter.emit(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the edited message and update the chat.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @param {NgForm} form - The form containing the edited message.
|
||||||
|
*/
|
||||||
|
onSubmit(chatId: string, form: NgForm) {
|
||||||
|
this.chatService.updateChat(chatId, form.value);
|
||||||
|
this.closeEditMsg();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the message when the Enter key is pressed.
|
||||||
|
* @param {KeyboardEvent} e - The keyboard event.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @param {NgForm} form - The form containing the message.
|
||||||
|
*/
|
||||||
|
sendMessageWithEnter(e: KeyboardEvent, chatId: string, form: NgForm) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
this.onSubmit(chatId, form);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EMOJI
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an emoji to the message.
|
||||||
|
* @param {any} $event - The emitted event containing the emoji.
|
||||||
|
*/
|
||||||
|
emojiOutputEmitter($event: any) {
|
||||||
|
this.addEmoji($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an emoji to the message.
|
||||||
|
* @param {any} event - The emitted event containing the emoji.
|
||||||
|
*/
|
||||||
|
public addEmoji(event: any) {
|
||||||
|
this.chat.message = `${this.chat.message}${event}`;
|
||||||
|
this.isEmojiPickerVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the visibility of the emoji picker.
|
||||||
|
*/
|
||||||
|
toggleEmojiPicker() {
|
||||||
|
this.isEmojiPickerVisible = !this.isEmojiPickerVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FILES
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the file in a new tab.
|
||||||
|
* @param {string} filePath - The path to the file.
|
||||||
|
*/
|
||||||
|
showCurrentFile(filePath: string) {
|
||||||
|
const url = filePath;
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the type of file based on its extension.
|
||||||
|
* @param {string} filePath - The path to the file.
|
||||||
|
* @returns {string} - The path to the corresponding file icon.
|
||||||
|
*/
|
||||||
|
getFileType(filePath: string): string {
|
||||||
|
const fileName = filePath.split('?')[0].split('/').pop();
|
||||||
|
|
||||||
|
if (fileName) {
|
||||||
|
if (fileName.endsWith('.mp3')) {
|
||||||
|
return 'assets/img/mp3Icon.svg';
|
||||||
|
} else if (fileName.endsWith('.jpg' || '.jpeg' || '.png' || '.gif')) {
|
||||||
|
return 'assets/img/imgIcon.svg';
|
||||||
|
} else if (fileName.endsWith('.pdf' || '.doc' || '.txt')) {
|
||||||
|
return 'assets/img/pdfIcon.svg';
|
||||||
|
} else if (fileName.endsWith('.mp4' || '.avi')) {
|
||||||
|
return 'assets/img/videoIcon.svg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'assets/img/documentIcon.svg';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file.
|
||||||
|
* @param {string} file - The file to be deleted.
|
||||||
|
*/
|
||||||
|
deleteFile(file: string) {
|
||||||
|
console.log('Deleted:' + file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
<section
|
||||||
|
(mouseleave)="this.isNavOpen = false; this.isEmojiPickerVisible = false"
|
||||||
|
[ngClass]="{ mirror: user.id === userService.getCurrentUserId() }"
|
||||||
|
>
|
||||||
|
@if (!openOnSecondaryChat) {
|
||||||
|
<div class="content">
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'okay.svg'"
|
||||||
|
[imgSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '24px' : '17px'"
|
||||||
|
[btnSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '36px' : '25px'"
|
||||||
|
[imgFilter]="'none'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
[ngClass]="{ mirror: user.id === userService.getCurrentUserId() }"
|
||||||
|
(click)="addReactionIcon('okay', currentChat)"
|
||||||
|
>
|
||||||
|
</app-small-btn>
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'clap-hands.svg'"
|
||||||
|
[imgSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '24px' : '17px'"
|
||||||
|
[btnSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '36px' : '25px'"
|
||||||
|
[imgFilter]="'none'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
[ngClass]="{ mirror: user.id === userService.getCurrentUserId() }"
|
||||||
|
(click)="addReactionIcon('clap-hands', currentChat)"
|
||||||
|
>
|
||||||
|
</app-small-btn>
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'add-reaction.svg'"
|
||||||
|
[imgSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '24px' : '17px'"
|
||||||
|
[btnSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '36px' : '25px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
[ngClass]="{ mirror: user.id === userService.getCurrentUserId() }"
|
||||||
|
(click)="toggleEmojiPicker()"
|
||||||
|
>
|
||||||
|
</app-small-btn>
|
||||||
|
@if (!isPrivatChannel) {
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'answer.svg'"
|
||||||
|
[imgSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '24px' : '17px'"
|
||||||
|
[btnSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '36px' : '25px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
[ngClass]="{ mirror: user.id === userService.getCurrentUserId() }"
|
||||||
|
(click)="toggleSecondaryChat(currentChat)"
|
||||||
|
>
|
||||||
|
</app-small-btn>
|
||||||
|
} @if (user.id === userService.getCurrentUserId()) {
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'more-vertical.svg'"
|
||||||
|
[imgSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '24px' : '17px'"
|
||||||
|
[btnSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '36px' : '25px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
[ngClass]="{ mirror: user.id === userService.getCurrentUserId() }"
|
||||||
|
(click)="toggleNav()"
|
||||||
|
>
|
||||||
|
</app-small-btn>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav"
|
||||||
|
[ngClass]="{
|
||||||
|
'd-none': !isNavOpen,
|
||||||
|
'animation-coming-in': isNavOpen,
|
||||||
|
'animation-coming-out': !isNavOpen
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="navbar">
|
||||||
|
<p
|
||||||
|
[ngClass]="{ mirror: user.id === userService.getCurrentUserId() }"
|
||||||
|
(click)="editMsg()"
|
||||||
|
>
|
||||||
|
{{ "options-menu.edit" | translate }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
[ngClass]="{ mirror: user.id === userService.getCurrentUserId() }"
|
||||||
|
(click)="deleteMsg('chats')"
|
||||||
|
>
|
||||||
|
{{ "options-menu.delete" | translate }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="content">
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'add-reaction.svg'"
|
||||||
|
[imgSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '24px' : '17px'"
|
||||||
|
[btnSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '36px' : '25px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
[ngClass]="{ mirror: user.id === userService.getCurrentUserId() }"
|
||||||
|
(click)="toggleEmojiPicker()"
|
||||||
|
></app-small-btn>
|
||||||
|
@if (user.id === userService.getCurrentUserId()) {
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'more-vertical.svg'"
|
||||||
|
[imgSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '24px' : '17px'"
|
||||||
|
[btnSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '36px' : '25px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
[ngClass]="{ mirror: user.id === userService.getCurrentUserId() }"
|
||||||
|
(click)="toggleNav()"
|
||||||
|
>
|
||||||
|
</app-small-btn>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav"
|
||||||
|
[ngClass]="{
|
||||||
|
'd-none': !isNavOpen,
|
||||||
|
'animation-coming-in': isNavOpen,
|
||||||
|
'animation-coming-out': !isNavOpen
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="navbar">
|
||||||
|
<p
|
||||||
|
[ngClass]="{ mirror: user.id === userService.getCurrentUserId() }"
|
||||||
|
(click)="editMsg()"
|
||||||
|
>
|
||||||
|
{{ "options-menu.edit" | translate }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
[ngClass]="{ mirror: user.id === userService.getCurrentUserId() }"
|
||||||
|
(click)="deleteMsg('chat-answers')"
|
||||||
|
>
|
||||||
|
{{ "options-menu.delete" | translate }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @if(isEmojiPickerVisible) {
|
||||||
|
<app-emoji-picker
|
||||||
|
[output]="'id'"
|
||||||
|
[ngClass]="{
|
||||||
|
mirror: user.id === userService.getCurrentUserId(),
|
||||||
|
secondaryChatPickerOffset: openOnSecondaryChat
|
||||||
|
}"
|
||||||
|
(emojiOutputEmitter)="emojiOutputEmitter($event, currentChat)"
|
||||||
|
(emojiVisibleEmitter)="emojiVisibleEmitter($event)"
|
||||||
|
></app-emoji-picker>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
@import "./../../../../../styles.scss";
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
max-width: 256px;
|
||||||
|
width: fit-content;
|
||||||
|
height: 46px;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 3px 15px 3px 20px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
border-top-left-radius: 25px;
|
||||||
|
border-top-right-radius: 25px;
|
||||||
|
border-bottom-left-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: fit-content;
|
||||||
|
white-space: nowrap;
|
||||||
|
position: absolute;
|
||||||
|
top: 52px;
|
||||||
|
right: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
border-top-left-radius: 30px;
|
||||||
|
border-bottom-left-radius: 30px;
|
||||||
|
border-bottom-right-radius: 30px;
|
||||||
|
p {
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
color: #545af1;
|
||||||
|
background-color: #edeefe;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #ed1f79;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-emoji-picker {
|
||||||
|
position: absolute;
|
||||||
|
top: 53px;
|
||||||
|
left: -80px;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-small-btn {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-none {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mirror {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryChatPickerOffset {
|
||||||
|
left: -190px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------- ANIMATION -------------*/
|
||||||
|
|
||||||
|
.animation-coming-in {
|
||||||
|
animation: coming-in 125ms ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-coming-out {
|
||||||
|
animation: coming-out 125ms ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes coming-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes coming-out {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------- RESPONSIVE -------------*/
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE) {
|
||||||
|
.content {
|
||||||
|
height: 32px;
|
||||||
|
padding: 2px 10px 2px 14px;
|
||||||
|
border-top-left-radius: 17px;
|
||||||
|
border-top-right-radius: 17px;
|
||||||
|
border-bottom-left-radius: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
top: 37px;
|
||||||
|
padding: 7px 14px;
|
||||||
|
border-top-left-radius: 21px;
|
||||||
|
border-bottom-left-radius: 21px;
|
||||||
|
border-bottom-right-radius: 21px;
|
||||||
|
p {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 7px 14px;
|
||||||
|
border-radius: 21px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { OptionsMenuComponent } from './options-menu.component';
|
||||||
|
|
||||||
|
describe('OptionsMenuComponent', () => {
|
||||||
|
let component: OptionsMenuComponent;
|
||||||
|
let fixture: ComponentFixture<OptionsMenuComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [OptionsMenuComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(OptionsMenuComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { SingleChatComponent } from '../single-chat.component';
|
||||||
|
import { SmallBtnComponent } from '../../../../shared/components/small-btn/small-btn.component';
|
||||||
|
import { ChatService } from '../../../../service/chat.service';
|
||||||
|
import { EmojiPickerComponent } from '../../../../shared/components/emoji-picker/emoji-picker.component';
|
||||||
|
import { UserService } from '../../../../service/user.service';
|
||||||
|
import { ChatReactions } from '../../../../interface/chat.interface';
|
||||||
|
import { User } from '../../../../interface/user.interface';
|
||||||
|
import { SharedService } from '../../../../service/shared.service';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-options-menu',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SingleChatComponent,
|
||||||
|
SmallBtnComponent,
|
||||||
|
EmojiPickerComponent,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
templateUrl: './options-menu.component.html',
|
||||||
|
styleUrl: './options-menu.component.scss',
|
||||||
|
})
|
||||||
|
export class OptionsMenuComponent {
|
||||||
|
@Input() user: User = {} as User;
|
||||||
|
@Input() currentChat: string = '';
|
||||||
|
@Input() openOnSecondaryChat: boolean = false;
|
||||||
|
@Input() isPrivatChannel: boolean = false;
|
||||||
|
@Input() viewWidth: number = 0;
|
||||||
|
@Output() editMsgEmitter: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
isNavOpen: boolean = false;
|
||||||
|
isEmojiPickerVisible: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public chatService: ChatService,
|
||||||
|
public userService: UserService,
|
||||||
|
private sharedService: SharedService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
RESPONSIVE_THRESHOLD_MOBILE = this.sharedService.RESPONSIVE_THRESHOLD_MOBILE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits signal to edit message and toggles navigation.
|
||||||
|
*/
|
||||||
|
editMsg() {
|
||||||
|
this.editMsgEmitter.emit(true);
|
||||||
|
this.toggleNav();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes message from specified database and toggles navigation.
|
||||||
|
* @param {string} database - The name of the database to delete from.
|
||||||
|
*/
|
||||||
|
deleteMsg(database: string) {
|
||||||
|
this.toggleNav();
|
||||||
|
this.chatService.deleteData(this.currentChat, database);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits emoji output event for a chat message.
|
||||||
|
* @param {*} $event - The event containing the emoji.
|
||||||
|
* @param {string} chatId - The ID of the chat message.
|
||||||
|
*/
|
||||||
|
emojiOutputEmitter($event: any, chatId: string) {
|
||||||
|
if (!this.checkExistEmojiOnChat(chatId, $event)) {
|
||||||
|
this.addNewReaction($event, chatId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds reaction icon to a chat message.
|
||||||
|
* @param {string} icon - The icon to add as reaction.
|
||||||
|
* @param {string} chatId - The ID of the chat message.
|
||||||
|
*/
|
||||||
|
addReactionIcon(icon: string, chatId: string) {
|
||||||
|
if (!this.checkExistEmojiOnChat(chatId, icon)) {
|
||||||
|
this.addNewReaction(icon, chatId);
|
||||||
|
} else {
|
||||||
|
const id = this.getReactionIcon(chatId, icon)[0].id;
|
||||||
|
if (id != undefined) {
|
||||||
|
this.toggleEmoji(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new reaction to a chat message.
|
||||||
|
* @param {*} event - The event containing the reaction.
|
||||||
|
* @param {string} chatId - The ID of the chat message.
|
||||||
|
*/
|
||||||
|
addNewReaction(event: any, chatId: string) {
|
||||||
|
let reaction: ChatReactions = {
|
||||||
|
chatId: chatId,
|
||||||
|
icon: event,
|
||||||
|
userId: [this.userService.getCurrentUserId()],
|
||||||
|
};
|
||||||
|
const { id, ...reactionWithoutId } = reaction;
|
||||||
|
this.chatService.createNewReaction(reactionWithoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a specific emoji exists on a chat message.
|
||||||
|
* @param {string} chatId - The ID of the chat message.
|
||||||
|
* @param {string} icon - The icon to check for existence.
|
||||||
|
* @returns {boolean} - True if the emoji exists, false otherwise.
|
||||||
|
*/
|
||||||
|
checkExistEmojiOnChat(chatId: string, icon: string) {
|
||||||
|
return this.getReaction(chatId).length > 0 &&
|
||||||
|
this.getReactionIcon(chatId, icon).length > 0
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all reactions for a chat message.
|
||||||
|
* @param {string} chatId - The ID of the chat message.
|
||||||
|
* @returns {Array} - Array of reactions for the chat message.
|
||||||
|
*/
|
||||||
|
getReaction(chatId: string) {
|
||||||
|
return this.chatService.allChatReactions.filter(
|
||||||
|
(reaction) => reaction.chatId === chatId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves reactions with a specific icon for a chat message.
|
||||||
|
* @param {string} chatId - The ID of the chat message.
|
||||||
|
* @param {string} icon - The icon to filter reactions.
|
||||||
|
* @returns {Array} - Array of reactions with the specified icon.
|
||||||
|
*/
|
||||||
|
getReactionIcon(chatId: string, icon: string) {
|
||||||
|
const chat = this.getReaction(chatId);
|
||||||
|
return chat.filter((reaction) => reaction.icon == icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits signal to toggle emoji picker visibility.
|
||||||
|
* @param {*} $event - The event containing the visibility state.
|
||||||
|
*/
|
||||||
|
emojiVisibleEmitter($event: any) {
|
||||||
|
this.isEmojiPickerVisible = $event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the navigation bar state.
|
||||||
|
*/
|
||||||
|
toggleNav() {
|
||||||
|
this.isNavOpen = !this.isNavOpen;
|
||||||
|
this.isEmojiPickerVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the secondary chat display.
|
||||||
|
* @param {string} chatId - The ID of the chat message.
|
||||||
|
*/
|
||||||
|
toggleSecondaryChat(chatId: string) {
|
||||||
|
this.chatService.toggleSecondaryChat(chatId);
|
||||||
|
this.isEmojiPickerVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of the emoji picker.
|
||||||
|
*/
|
||||||
|
toggleEmojiPicker() {
|
||||||
|
this.isEmojiPickerVisible = !this.isEmojiPickerVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the document ID for a reaction.
|
||||||
|
* @param {string} chatId - The ID of the chat message.
|
||||||
|
* @returns {Array} - Array containing the reaction document ID.
|
||||||
|
*/
|
||||||
|
getReactionDocId(chatId: string) {
|
||||||
|
return this.chatService.allChatReactions.filter(
|
||||||
|
(reaction) => reaction.id === chatId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the reaction of a user on a chat message.
|
||||||
|
* @param {string} reactionID - The ID of the reaction.
|
||||||
|
*/
|
||||||
|
toggleEmoji(reactionID: string) {
|
||||||
|
const userIds = this.getReactionDocId(reactionID)[0].userId;
|
||||||
|
if (userIds.includes(this.userService.getCurrentUserId())) {
|
||||||
|
userIds.splice(userIds.indexOf(this.userService.getCurrentUserId()), 1);
|
||||||
|
if (userIds.length == 0) {
|
||||||
|
this.chatService.deleteData(reactionID, 'reactions');
|
||||||
|
} else {
|
||||||
|
this.chatService.updateReaction(reactionID, userIds);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userIds.push(this.userService.getCurrentUserId());
|
||||||
|
this.chatService.updateReaction(reactionID, userIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
<section
|
||||||
|
(mouseleave)="this.isEmojiPickerVisible = false"
|
||||||
|
[ngStyle]="{
|
||||||
|
'flex-direction':
|
||||||
|
user.id === userService.getCurrentUserId() ? 'row-reverse' : 'row'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'add-reaction.svg'"
|
||||||
|
[imgSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '24px' : '21px'"
|
||||||
|
[btnSize]="viewWidth >= RESPONSIVE_THRESHOLD_MOBILE ? '40px' : '34px'"
|
||||||
|
[btnBgColor]="'#fff'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
(click)="toggleEmojiPicker()"
|
||||||
|
>
|
||||||
|
</app-small-btn>
|
||||||
|
@if(isEmojiPickerVisible) {
|
||||||
|
<app-emoji-picker
|
||||||
|
[ngClass]="{
|
||||||
|
emojiPickerFirstMsg: secondaryChatFirstMsg,
|
||||||
|
emojiPickerAnswerMsg: !secondaryChatFirstMsg
|
||||||
|
}"
|
||||||
|
[output]="'id'"
|
||||||
|
(emojiOutputEmitter)="emojiOutputEmitter($event, chat.id)"
|
||||||
|
(emojiVisibleEmitter)="emojiVisibleEmitter($event)"
|
||||||
|
[ngStyle]="{
|
||||||
|
left: user.id === userService.getCurrentUserId() ? 'unset' : '',
|
||||||
|
right: user.id === userService.getCurrentUserId() ? '20px' : '',
|
||||||
|
|
||||||
|
}"
|
||||||
|
></app-emoji-picker>
|
||||||
|
}
|
||||||
|
<div class="emojiSection">
|
||||||
|
@for (reaction of getReaction(chat.id); track reaction; let i = $index) {
|
||||||
|
@if (reaction.id) {
|
||||||
|
<div
|
||||||
|
class="emoji"
|
||||||
|
(click)="toggleEmoji(reaction.id)"
|
||||||
|
(mousemove)="openDialog(reaction.id, $event)"
|
||||||
|
(mouseleave)="closeDialog()"
|
||||||
|
>
|
||||||
|
@if (reaction.icon === "okay" || reaction.icon === "clap-hands") {
|
||||||
|
<img src="./assets/img/{{ reaction.icon }}.svg" />
|
||||||
|
} @else {
|
||||||
|
<ngx-emoji
|
||||||
|
emoji="{{ reaction.icon }}"
|
||||||
|
[size]="viewWidth <= RESPONSIVE_THRESHOLD_MOBILE ? 20 : 24"
|
||||||
|
></ngx-emoji>
|
||||||
|
|
||||||
|
}
|
||||||
|
<span>{{ reaction.userId.length }}</span>
|
||||||
|
</div>
|
||||||
|
} }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DIALOG-->
|
||||||
|
|
||||||
|
@if (reactionDialogId != '') { @for (reaction of
|
||||||
|
getReactionDocId(reactionDialogId); track reaction) {
|
||||||
|
<div
|
||||||
|
class="dialog"
|
||||||
|
[style.left.px]="dialogX"
|
||||||
|
[style.top.px]="dialogY"
|
||||||
|
(click)="reactionDialogId = ''"
|
||||||
|
>
|
||||||
|
<div class="icon">
|
||||||
|
@if (reaction.icon === "okay" || reaction.icon === "clap-hands") {
|
||||||
|
<img src="./assets/img/{{ reaction.icon }}.svg" />
|
||||||
|
} @else {
|
||||||
|
<ngx-emoji emoji="{{ reaction.icon }}"></ngx-emoji>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="name">
|
||||||
|
@for (reactionUId of getReactionDocId(reactionDialogId)[0].userId; track
|
||||||
|
reactionUId;) {
|
||||||
|
<div class="user">
|
||||||
|
{{ getUserId(reactionUId)[0].firstName }}
|
||||||
|
{{ getUserId(reactionUId)[0].lastName }}
|
||||||
|
@if (getUserId(reactionUId)[0].id == userService.getCurrentUserId()) {
|
||||||
|
(You)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (getReactionDocId(reactionDialogId)[0].userId.length > 1) {
|
||||||
|
<p>{{ "reaction-emojis.react" | translate }}</p>
|
||||||
|
} @else {
|
||||||
|
<p>{{ "reaction-emojis.react2" | translate }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} }
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
@import "../../../../../styles.scss";
|
||||||
|
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emojiSection {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
width: 44px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid #adb0d9;
|
||||||
|
transition: 0.125s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
img {
|
||||||
|
height: 24px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
border-color: #444df2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
position: fixed;
|
||||||
|
min-width: 158px;
|
||||||
|
width: fit-content;
|
||||||
|
min-height: 112px;
|
||||||
|
height: fit-content;
|
||||||
|
max-height: 300px;
|
||||||
|
padding: 15px 10px;
|
||||||
|
border-radius: 30px 0px 30px 30px;
|
||||||
|
z-index: 5;
|
||||||
|
background-color: #444df2;
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 6px 0;
|
||||||
|
img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
padding: 6px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
.user {
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mirror {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emojiPickerFirstMsg {
|
||||||
|
position: absolute;
|
||||||
|
top: 50px;
|
||||||
|
left: 20px;
|
||||||
|
z-index: 3;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emojiPickerAnswerMsg {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 50px;
|
||||||
|
left: 20px;
|
||||||
|
z-index: 3;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngx-emoji {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-small-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: top;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------- RESPONSIVE -------------*/
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE) {
|
||||||
|
.emoji {
|
||||||
|
width: 36px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
img {
|
||||||
|
height: 20px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngx-emoji {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 20px !important;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ReactionEmojisComponent } from './reaction-emojis.component';
|
||||||
|
|
||||||
|
describe('ReactionEmojisComponent', () => {
|
||||||
|
let component: ReactionEmojisComponent;
|
||||||
|
let fixture: ComponentFixture<ReactionEmojisComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ReactionEmojisComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ReactionEmojisComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,225 @@
|
||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
HostListener,
|
||||||
|
Input,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { SmallBtnComponent } from '../../../../shared/components/small-btn/small-btn.component';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import {
|
||||||
|
Chat,
|
||||||
|
ChatAnswers,
|
||||||
|
ChatReactions,
|
||||||
|
} from '../../../../interface/chat.interface';
|
||||||
|
import { UserService } from '../../../../service/user.service';
|
||||||
|
import { ChatService } from '../../../../service/chat.service';
|
||||||
|
import { ChannleService } from '../../../../service/channle.service';
|
||||||
|
import { EmojiPickerComponent } from '../../../../shared/components/emoji-picker/emoji-picker.component';
|
||||||
|
import { EmojiComponent } from '@ctrl/ngx-emoji-mart/ngx-emoji';
|
||||||
|
import { timeInterval } from 'rxjs';
|
||||||
|
import { User } from '../../../../interface/user.interface';
|
||||||
|
import { SharedService } from '../../../../service/shared.service';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-reaction-emojis',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SmallBtnComponent,
|
||||||
|
EmojiPickerComponent,
|
||||||
|
EmojiComponent,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
templateUrl: './reaction-emojis.component.html',
|
||||||
|
styleUrl: './reaction-emojis.component.scss',
|
||||||
|
})
|
||||||
|
export class ReactionEmojisComponent {
|
||||||
|
@Input() user: User = {} as User;
|
||||||
|
@Input() chat: Chat | ChatAnswers = {} as Chat | ChatAnswers;
|
||||||
|
@Input() openOnSecondaryChat: boolean = false;
|
||||||
|
@Input() secondaryChatFirstMsg: boolean = false;
|
||||||
|
@Input() viewWidth: number = 0;
|
||||||
|
|
||||||
|
reactionDialogId: string = '';
|
||||||
|
reactionDialogLeft = 0;
|
||||||
|
isEmojiPickerVisible: boolean = false;
|
||||||
|
emojiSectionWidth: number = 0;
|
||||||
|
dialogX: number = 0;
|
||||||
|
dialogY: number = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private elementRef: ElementRef,
|
||||||
|
public userService: UserService,
|
||||||
|
private chatService: ChatService,
|
||||||
|
private sharedService: SharedService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
RESPONSIVE_THRESHOLD_MOBILE = this.sharedService.RESPONSIVE_THRESHOLD_MOBILE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an emoji output event.
|
||||||
|
* @param {any} $event - The emoji event.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
*/
|
||||||
|
emojiOutputEmitter($event: any, chatId: string) {
|
||||||
|
if (!this.checkExistEmojiOnChat(chatId, $event)) {
|
||||||
|
let reaction: ChatReactions = {
|
||||||
|
chatId: chatId,
|
||||||
|
icon: $event,
|
||||||
|
userId: [this.userService.getCurrentUserId()],
|
||||||
|
};
|
||||||
|
const { id, ...reactionWithoutId } = reaction;
|
||||||
|
this.chatService.createNewReaction(reactionWithoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event indicating the visibility of the emoji picker.
|
||||||
|
* @param {any} $event - The visibility event.
|
||||||
|
*/
|
||||||
|
emojiVisibleEmitter($event: any) {
|
||||||
|
this.isEmojiPickerVisible = $event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a dialog for a reaction.
|
||||||
|
* @param {any} reactionId - The ID of the reaction.
|
||||||
|
* @param {MouseEvent} event - The mouse event triggering the dialog.
|
||||||
|
*/
|
||||||
|
openDialog(reactionId: any, event: MouseEvent) {
|
||||||
|
this.reactionDialogId = reactionId;
|
||||||
|
this.updateDialogPosition(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the currently opened dialog.
|
||||||
|
*/
|
||||||
|
closeDialog() {
|
||||||
|
this.reactionDialogId = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the position of the dialog based on the mouse event.
|
||||||
|
* @param {MouseEvent} event - The mouse event triggering the update.
|
||||||
|
*/
|
||||||
|
updateDialogPosition(event: MouseEvent) {
|
||||||
|
const currentTarget = event.currentTarget as HTMLElement;
|
||||||
|
if (currentTarget) {
|
||||||
|
const rect = currentTarget.getBoundingClientRect();
|
||||||
|
this.dialogX = event.clientX - 200;
|
||||||
|
this.dialogY = event.clientY + 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given emoji exists on a chat.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @param {string} icon - The icon to check.
|
||||||
|
* @returns {boolean} True if the emoji exists, otherwise false.
|
||||||
|
*/
|
||||||
|
checkExistEmojiOnChat(chatId: string, icon: string) {
|
||||||
|
return this.getReaction(chatId).length > 0 &&
|
||||||
|
this.getReactionIcon(chatId, icon).length > 0
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the index of an element in an array.
|
||||||
|
* @param {any[]} array - The array to search in.
|
||||||
|
* @param {any} element - The element to find the index of.
|
||||||
|
* @returns {number} The index of the element in the array.
|
||||||
|
*/
|
||||||
|
indexOfArray(array: any[], element: any): number {
|
||||||
|
return array.indexOf(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all reactions for a given chat.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @returns {ChatReactions[]} All reactions for the chat.
|
||||||
|
*/
|
||||||
|
getReaction(chatId: string) {
|
||||||
|
return this.chatService.allChatReactions.filter(
|
||||||
|
(reaction) => reaction.chatId === chatId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves reactions with a specific icon for a given chat.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @param {string} icon - The icon to filter by.
|
||||||
|
* @returns {ChatReactions[]} Reactions with the specified icon for the chat.
|
||||||
|
*/
|
||||||
|
getReactionIcon(chatId: string, icon: string) {
|
||||||
|
const chat = this.getReaction(chatId);
|
||||||
|
return chat.filter((reaction) => reaction.icon == icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the reaction document ID for a given chat.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @returns {ChatReactions[]} The reaction document ID for the chat.
|
||||||
|
*/
|
||||||
|
getReactionDocId(chatId: string) {
|
||||||
|
return this.chatService.allChatReactions.filter(
|
||||||
|
(reaction) => reaction.id === chatId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the number of reaction document IDs for a given chat.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @returns {number} The count of reaction document IDs for the chat.
|
||||||
|
*/
|
||||||
|
countReactionDocId(chatId: string) {
|
||||||
|
let count = 0;
|
||||||
|
this.chatService.allChatReactions.forEach((reaction) => {
|
||||||
|
if (reaction.id === chatId) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves user information based on user ID.
|
||||||
|
* @param {string} userId - The ID of the user.
|
||||||
|
* @returns {User[]} User information for the given user ID.
|
||||||
|
*/
|
||||||
|
getUserId(userId: string) {
|
||||||
|
const filteredUser = this.userService
|
||||||
|
.getUsers()
|
||||||
|
.filter((user) => user.id == userId);
|
||||||
|
return filteredUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the presence of a user's reaction to a chat.
|
||||||
|
* @param {string} reactionID - The ID of the reaction.
|
||||||
|
*/
|
||||||
|
toggleEmoji(reactionID: string) {
|
||||||
|
const userIds = this.getReactionDocId(reactionID)[0].userId;
|
||||||
|
if (userIds.includes(this.userService.getCurrentUserId())) {
|
||||||
|
userIds.splice(userIds.indexOf(this.userService.getCurrentUserId()), 1);
|
||||||
|
if (userIds.length == 0) {
|
||||||
|
this.chatService.deleteData(reactionID, 'reactions');
|
||||||
|
} else {
|
||||||
|
this.chatService.updateReaction(reactionID, userIds);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userIds.push(this.userService.getCurrentUserId());
|
||||||
|
this.chatService.updateReaction(reactionID, userIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of the emoji picker.
|
||||||
|
*/
|
||||||
|
toggleEmojiPicker() {
|
||||||
|
this.isEmojiPickerVisible = !this.isEmojiPickerVisible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
<section
|
||||||
|
(mousemove)="showOptionMenu()"
|
||||||
|
(mouseleave)="hideOptionMenu()"
|
||||||
|
[ngStyle]="{
|
||||||
|
'justify-content':
|
||||||
|
user.id === userService.getCurrentUserId() ? 'flex-end' : 'flex-start'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
@if (firstLoadOptionMenu) {
|
||||||
|
<app-options-menu
|
||||||
|
[ngClass]="{
|
||||||
|
menuRightSide: user.id !== userService.getCurrentUserId(),
|
||||||
|
menuLeftSide: user.id === userService.getCurrentUserId(),
|
||||||
|
'animation-coming-in-left':
|
||||||
|
isOptionMenuVisible && user.id === userService.getCurrentUserId(),
|
||||||
|
'animation-coming-out-left':
|
||||||
|
!isOptionMenuVisible && user.id === userService.getCurrentUserId(),
|
||||||
|
'animation-coming-in-right':
|
||||||
|
isOptionMenuVisible && user.id !== userService.getCurrentUserId(),
|
||||||
|
'animation-coming-out-right':
|
||||||
|
!isOptionMenuVisible && user.id !== userService.getCurrentUserId()
|
||||||
|
}"
|
||||||
|
[user]="user"
|
||||||
|
[currentChat]="currentChat"
|
||||||
|
(editMsgEmitter)="editMsgEmitter($event)"
|
||||||
|
[openOnSecondaryChat]="openOnSecondaryChat"
|
||||||
|
[isPrivatChannel]="isPrivatChannel"
|
||||||
|
[viewWidth]="viewWidth"
|
||||||
|
>
|
||||||
|
></app-options-menu
|
||||||
|
>
|
||||||
|
}
|
||||||
|
<div
|
||||||
|
class="chatBox"
|
||||||
|
[ngStyle]="{
|
||||||
|
'flex-direction':
|
||||||
|
user.id === userService.getCurrentUserId() ? 'row-reverse' : 'row'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="userAvatar"
|
||||||
|
[ngStyle]="{
|
||||||
|
padding:
|
||||||
|
user.id === userService.getCurrentUserId()
|
||||||
|
? '0 0 0 24px'
|
||||||
|
: '0 24px 0 0'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<img src="{{ user.avatar }}" alt="" />
|
||||||
|
</div>
|
||||||
|
@if (!isMsgEditFormOpen) {
|
||||||
|
<div
|
||||||
|
class="chat"
|
||||||
|
[ngStyle]="{
|
||||||
|
'align-items':
|
||||||
|
user.id === userService.getCurrentUserId() ? 'flex-end' : ''
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="name"
|
||||||
|
[ngStyle]="{
|
||||||
|
'flex-direction':
|
||||||
|
user.id === userService.getCurrentUserId() ? 'row-reverse' : 'row',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ user.firstName }} {{ user.lastName }}
|
||||||
|
<span
|
||||||
|
[ngStyle]="{
|
||||||
|
padding:
|
||||||
|
user.id === userService.getCurrentUserId()
|
||||||
|
? '0 24px 0 0'
|
||||||
|
: '0 0 0 24px'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
@if(openOnSecondaryChat) {
|
||||||
|
{{ convertTimestampDate(chat.publishedTimestamp) }} }
|
||||||
|
{{ convertTimestampHour(chat.publishedTimestamp) }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="content"
|
||||||
|
[ngClass]="{
|
||||||
|
'content-mirror': user.id === userService.getCurrentUserId()
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="msg">
|
||||||
|
{{ chat.message }}
|
||||||
|
@if (chat.edited) {
|
||||||
|
<span>{{ "single-chat.edit" | translate }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<app-attachments
|
||||||
|
[chatId]="chat.id"
|
||||||
|
[openOnSecondaryChat]="openOnSecondaryChat"
|
||||||
|
[viewWidth]="viewWidth"
|
||||||
|
></app-attachments>
|
||||||
|
</div>
|
||||||
|
@if (!isPrivatChannel) {
|
||||||
|
<div class="answers">
|
||||||
|
@if (chatService.getChatAnswers(chat.id).length > 0 && showAnswer) {
|
||||||
|
@if(chatService.getChatAnswers(chat.id).length == 1) {
|
||||||
|
<p (click)="openSecondaryChat(chat.id)">
|
||||||
|
{{ displayCountChatAnswer() }}
|
||||||
|
{{ "single-chat.answer2" | translate }}
|
||||||
|
</p>
|
||||||
|
<span
|
||||||
|
>{{ "single-chat.answer3" | translate }}
|
||||||
|
{{ displayLastChatAnswer() }}</span
|
||||||
|
>
|
||||||
|
} @else {
|
||||||
|
<p (click)="openSecondaryChat(chat.id)">
|
||||||
|
{{ displayCountChatAnswer() }}
|
||||||
|
{{ "single-chat.answer" | translate }}
|
||||||
|
</p>
|
||||||
|
<span
|
||||||
|
>{{ "single-chat.answer3" | translate }}
|
||||||
|
{{ displayLastChatAnswer() }}</span
|
||||||
|
>
|
||||||
|
} } @else if (showAnswer) {
|
||||||
|
<p (click)="openSecondaryChat(chat.id)">
|
||||||
|
{{ "single-chat.answer4" | translate }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="spacer"></div>
|
||||||
|
}
|
||||||
|
<app-reaction-emojis
|
||||||
|
[chat]="chat"
|
||||||
|
[user]="user"
|
||||||
|
[viewWidth]="viewWidth"
|
||||||
|
[openOnSecondaryChat]="openOnSecondaryChat"
|
||||||
|
[secondaryChatFirstMsg]="secondaryChatFirstMsg"
|
||||||
|
></app-reaction-emojis>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="editForm">
|
||||||
|
<app-edit-msg
|
||||||
|
[chat]="chat"
|
||||||
|
[viewWidth]="viewWidth"
|
||||||
|
[openOnSecondaryChat]="openOnSecondaryChat"
|
||||||
|
(closeEditMsgEmitter)="closeEditMsgEmitter($event)"
|
||||||
|
></app-edit-msg>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
@import "../../../../styles.scss";
|
||||||
|
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 32px 48px;
|
||||||
|
position: relative;
|
||||||
|
&:hover {
|
||||||
|
background-color: #edeefe;
|
||||||
|
.userContent {
|
||||||
|
.content {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuRightSide {
|
||||||
|
position: absolute;
|
||||||
|
top: -24px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuLeftSide {
|
||||||
|
position: absolute;
|
||||||
|
top: -24px;
|
||||||
|
left: 12px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatBox {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAvatar {
|
||||||
|
img {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat {
|
||||||
|
.name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
border-top-right-radius: 24px;
|
||||||
|
border-bottom-left-radius: 24px;
|
||||||
|
border-bottom-right-radius: 24px;
|
||||||
|
background-color: #eceefe;
|
||||||
|
.msg {
|
||||||
|
position: relative;
|
||||||
|
width: fit-content;
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app-attachments {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-mirror {
|
||||||
|
color: #fff;
|
||||||
|
border-top-left-radius: 24px;
|
||||||
|
border-top-right-radius: 0 !important;
|
||||||
|
background-color: #797ef3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answers {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 12px 0 6px 0;
|
||||||
|
p {
|
||||||
|
margin-right: 2px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #535af1;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
margin-right: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #444df2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
padding-left: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
margin: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editForm {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------- ANIMATION -------------*/
|
||||||
|
|
||||||
|
.animation-coming-in-left {
|
||||||
|
animation: coming-in-left 125ms ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-coming-in-right {
|
||||||
|
animation: coming-in-right 125ms ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-coming-out-left {
|
||||||
|
animation: coming-out-left 125ms ease-in-out forwards;
|
||||||
|
}
|
||||||
|
.animation-coming-out-right {
|
||||||
|
animation: coming-out-right 125ms ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes coming-in-left {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-12px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes coming-out-left {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-12px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes coming-in-right {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(12px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes coming-out-right {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(12px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------- RESPONSIVE -------------*/
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE) {
|
||||||
|
section {
|
||||||
|
padding: 24px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAvatar {
|
||||||
|
img {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answers {
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SingleChatComponent } from './single-chat.component';
|
||||||
|
|
||||||
|
describe('SingleChatComponent', () => {
|
||||||
|
let component: SingleChatComponent;
|
||||||
|
let fixture: ComponentFixture<SingleChatComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SingleChatComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SingleChatComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { User } from '../../../interface/user.interface';
|
||||||
|
import { Chat, ChatAnswers } from '../../../interface/chat.interface';
|
||||||
|
import { ChatContentComponent } from '../chat-content/chat-content.component';
|
||||||
|
import { CommonModule, NgSwitchCase } from '@angular/common';
|
||||||
|
import { ChatService } from '../../../service/chat.service';
|
||||||
|
import { DownloadFilesService } from '../../../service/download-files.service';
|
||||||
|
import { NgxExtendedPdfViewerModule } from 'ngx-extended-pdf-viewer';
|
||||||
|
import { OptionsMenuComponent } from './options-menu/options-menu.component';
|
||||||
|
import { AttachmentsComponent } from './attachments/attachments.component';
|
||||||
|
import { ChatMsgBoxComponent } from '../chat-msg-box/chat-msg-box.component';
|
||||||
|
import { SmallBtnComponent } from '../../../shared/components/small-btn/small-btn.component';
|
||||||
|
import { FormsModule, NgForm } from '@angular/forms';
|
||||||
|
import { PickerComponent } from '@ctrl/ngx-emoji-mart';
|
||||||
|
import { EditMsgComponent } from './edit-msg/edit-msg.component';
|
||||||
|
import { ReactionEmojisComponent } from './reaction-emojis/reaction-emojis.component';
|
||||||
|
import { UserService } from '../../../service/user.service';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-single-chat',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ChatContentComponent,
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
NgSwitchCase,
|
||||||
|
NgxExtendedPdfViewerModule,
|
||||||
|
OptionsMenuComponent,
|
||||||
|
AttachmentsComponent,
|
||||||
|
ChatMsgBoxComponent,
|
||||||
|
SmallBtnComponent,
|
||||||
|
PickerComponent,
|
||||||
|
EditMsgComponent,
|
||||||
|
ReactionEmojisComponent,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
templateUrl: './single-chat.component.html',
|
||||||
|
styleUrl: './single-chat.component.scss',
|
||||||
|
})
|
||||||
|
export class SingleChatComponent {
|
||||||
|
@Input() user: User = {} as User;
|
||||||
|
@Input() chat: Chat | ChatAnswers = {} as Chat | ChatAnswers;
|
||||||
|
@Input() index: number = 0;
|
||||||
|
@Input() currentChat: string = '';
|
||||||
|
@Input() isPrivatChannel: boolean = false;
|
||||||
|
@Input() showAnswer: boolean = false;
|
||||||
|
@Input() openOnSecondaryChat: boolean = false;
|
||||||
|
@Input() secondaryChatFirstMsg: boolean = false;
|
||||||
|
@Input() viewWidth: number = 0;
|
||||||
|
|
||||||
|
trustedUrl: string = '';
|
||||||
|
isOptionMenuVisible: boolean = false;
|
||||||
|
isMsgEditFormOpen: boolean = false;
|
||||||
|
firstLoadOptionMenu: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public chatService: ChatService,
|
||||||
|
public channelService: ChatService,
|
||||||
|
public userService: UserService,
|
||||||
|
public downloadFilesService: DownloadFilesService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits a signal to edit a message.
|
||||||
|
* @param {boolean} variable - The value indicating whether the message edit form should be open or not.
|
||||||
|
*/
|
||||||
|
editMsgEmitter(variable: boolean) {
|
||||||
|
this.isMsgEditFormOpen = variable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits a signal to close the message edit form and hide the option menu.
|
||||||
|
* @param {boolean} value - The value indicating whether the message edit form should be closed.
|
||||||
|
*/
|
||||||
|
closeEditMsgEmitter(value: boolean) {
|
||||||
|
this.isMsgEditFormOpen = value;
|
||||||
|
this.hideOptionMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the option menu.
|
||||||
|
*/
|
||||||
|
showOptionMenu() {
|
||||||
|
this.isOptionMenuVisible = true;
|
||||||
|
this.firstLoadOptionMenu = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the option menu.
|
||||||
|
*/
|
||||||
|
hideOptionMenu() {
|
||||||
|
this.isOptionMenuVisible = false;
|
||||||
|
this.firstLoadOptionMenu = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the count of chat answers.
|
||||||
|
* @returns {number} The count of chat answers.
|
||||||
|
*/
|
||||||
|
displayCountChatAnswer() {
|
||||||
|
return this.chatService.getChatAnswers(this.chat.id).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the timestamp of the last chat answer in a formatted time.
|
||||||
|
* @returns {string|null} The formatted time of the last chat answer.
|
||||||
|
*/
|
||||||
|
displayLastChatAnswer() {
|
||||||
|
const getChatAnswers = this.chatService.getChatAnswers(this.chat.id);
|
||||||
|
const lastChatAnswer = getChatAnswers[getChatAnswers.length - 1];
|
||||||
|
if (lastChatAnswer) {
|
||||||
|
return this.convertTimestampHour(lastChatAnswer.publishedTimestamp);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a timestamp to a formatted time string.
|
||||||
|
* @param {number} timestamp - The timestamp to convert.
|
||||||
|
* @returns {string} The formatted time string.
|
||||||
|
*/
|
||||||
|
convertTimestampHour(timestamp: number) {
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
let hour = date.getHours();
|
||||||
|
const minute = ('0' + date.getMinutes()).slice(-2);
|
||||||
|
const period = hour >= 12 ? 'PM' : 'AM';
|
||||||
|
hour = hour % 12 || 12;
|
||||||
|
|
||||||
|
let hourWithNull = ('0' + hour).slice(-2);
|
||||||
|
|
||||||
|
const formattedTime = `${hourWithNull}:${minute} ${period}`;
|
||||||
|
return formattedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a timestamp to a formatted date string.
|
||||||
|
* @param {number} timestamp - The timestamp to convert.
|
||||||
|
* @returns {string} The formatted date string.
|
||||||
|
*/
|
||||||
|
convertTimestampDate(timestamp: number) {
|
||||||
|
const currentDate = new Date();
|
||||||
|
const inputDate = new Date(timestamp * 1000);
|
||||||
|
const months = [
|
||||||
|
'Jan.',
|
||||||
|
'Feb.',
|
||||||
|
'Mar.',
|
||||||
|
'Apr.',
|
||||||
|
'May.',
|
||||||
|
'Jun.',
|
||||||
|
'Jul.',
|
||||||
|
'Aug.',
|
||||||
|
'Sep.',
|
||||||
|
'Oct.',
|
||||||
|
'Nov.',
|
||||||
|
'Dec.',
|
||||||
|
];
|
||||||
|
|
||||||
|
const dayNumber = inputDate.getDate();
|
||||||
|
const month = months[inputDate.getMonth()];
|
||||||
|
|
||||||
|
if (inputDate.toDateString() === currentDate.toDateString()) {
|
||||||
|
return `Today`;
|
||||||
|
} else {
|
||||||
|
return `${dayNumber} ${month}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a secondary chat.
|
||||||
|
* @param {string} chatId - The ID of the secondary chat to open.
|
||||||
|
*/
|
||||||
|
openSecondaryChat(chatId: string) {
|
||||||
|
this.chatService.toggleSecondaryChat(chatId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<div class="avatarSektion">
|
||||||
|
<app-start-header [display]="'none'"></app-start-header>
|
||||||
|
<div class="avatarContainer">
|
||||||
|
<div class="avatarTop">
|
||||||
|
<div routerLink="/register" class="registerBackImg">
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'login/arrow-back.svg'"
|
||||||
|
[imgSize]="'24px'"
|
||||||
|
[btnSize]="'48px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
>
|
||||||
|
</app-small-btn>
|
||||||
|
</div>
|
||||||
|
<span>{{ 'chooseAvatar.header' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="viewAvatarContainer">
|
||||||
|
<img [src]="avatarSrc" alt="Avatar" />
|
||||||
|
<span>{{ loginService.firstName }} {{ loginService.lastName }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
#fileInput
|
||||||
|
id="file"
|
||||||
|
style="display: none"
|
||||||
|
(change)="onFileChange($event)"
|
||||||
|
accept="image/*"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="chooseAvatarContainer">
|
||||||
|
<div><span>{{ 'chooseAvatar.avatarInfo' | translate }}</span></div>
|
||||||
|
<div class="mobileImgContainer">
|
||||||
|
<img
|
||||||
|
(click)="chooseExistAvatar(i)"
|
||||||
|
*ngFor="let imageSrc of avatarImages; let i = index"
|
||||||
|
[src]="imageSrc"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ownAvatarContainer">
|
||||||
|
<div>{{ 'chooseAvatar.ownAvatar' | translate }}</div>
|
||||||
|
<div>
|
||||||
|
<button class="BtnUpload" (click)="fileInput.click()">
|
||||||
|
{{ 'chooseAvatar.avatarBtn' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="avatarBottom">
|
||||||
|
<button class="hoverBtn" (click)="loginService.register()">{{ 'chooseAvatar.Btn' | translate }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-footer></app-footer>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
@import "../../../shared/components/login/mixins/mixin.scss";
|
||||||
|
* {
|
||||||
|
font-family: Nunito;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarSektion {
|
||||||
|
@include SektionBody;
|
||||||
|
}
|
||||||
|
.avatarContainer {
|
||||||
|
@include Card;
|
||||||
|
}
|
||||||
|
.avatarTop {
|
||||||
|
@include topText;
|
||||||
|
> span {
|
||||||
|
width: 90%;
|
||||||
|
padding-right: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.viewAvatarContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
> img {
|
||||||
|
height: 168px;
|
||||||
|
width: 168px;
|
||||||
|
border-radius: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
> span {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chooseAvatarContainer {
|
||||||
|
gap: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
> span {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
> img {
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 100%;
|
||||||
|
border: 4px solid #ffff;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
border: 4px solid #cacad6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ownAvatarContainer {
|
||||||
|
display: flex;
|
||||||
|
gap: 32px;
|
||||||
|
margin-right: 67px;
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.BtnUpload {
|
||||||
|
background-color: #ffff;
|
||||||
|
color: #444df2;
|
||||||
|
border: 1px solid #444df2;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: #444df2;
|
||||||
|
color: #ffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarBottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@include buttonGrey;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.hoverBtn {
|
||||||
|
&:hover {
|
||||||
|
background-color: #676eec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 570px) {
|
||||||
|
.avatarTop {
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font-size: 32px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.viewAvatarContainer {
|
||||||
|
> img {
|
||||||
|
height: 148px;
|
||||||
|
width: 148px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chooseAvatarContainer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.mobileImgContainer {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.ownAvatarContainer {
|
||||||
|
margin-right: 0;
|
||||||
|
gap: 16px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.avatarBottom {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.BtnUpload {
|
||||||
|
&:hover {
|
||||||
|
background-color: #ffff;
|
||||||
|
color: #444df2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hoverBtn {
|
||||||
|
background-color: #676eec;
|
||||||
|
}
|
||||||
|
@media (max-width: 380px) {
|
||||||
|
.avatarContainer {
|
||||||
|
max-width: 306px;
|
||||||
|
}
|
||||||
|
.avatarTop {
|
||||||
|
width: 95%;
|
||||||
|
> span {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ChooseAvatarComponent } from './choose-avatar.component';
|
||||||
|
|
||||||
|
describe('ChooseAvatarComponent', () => {
|
||||||
|
let component: ChooseAvatarComponent;
|
||||||
|
let fixture: ComponentFixture<ChooseAvatarComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ChooseAvatarComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ChooseAvatarComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { FooterComponent } from '../../../shared/components/login/footer/footer.component';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { SmallBtnComponent } from '../../../shared/components/small-btn/small-btn.component';
|
||||||
|
import { getStorage, ref, uploadBytes, getDownloadURL } from 'firebase/storage';
|
||||||
|
import { Firestore } from '@angular/fire/firestore';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { loginService } from '../../../service/login.service';
|
||||||
|
import { StartHeaderComponent } from '../../../shared/components/login/start-header/start-header.component';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-choose-avatar',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './choose-avatar.component.html',
|
||||||
|
styleUrl: './choose-avatar.component.scss',
|
||||||
|
imports: [
|
||||||
|
StartHeaderComponent,
|
||||||
|
FooterComponent,
|
||||||
|
RouterModule,
|
||||||
|
SmallBtnComponent,
|
||||||
|
CommonModule,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class ChooseAvatarComponent {
|
||||||
|
avatarSrc: string = './assets/img/user-icons/guest.svg';
|
||||||
|
firestore: Firestore = inject(Firestore);
|
||||||
|
selectedFile: File | null = null;
|
||||||
|
avatarImages: string[] = [
|
||||||
|
'./assets/img/user-icons/female-1.svg',
|
||||||
|
'./assets/img/user-icons/female-2.svg',
|
||||||
|
'./assets/img/user-icons/male-1.svg',
|
||||||
|
'./assets/img/user-icons/male-2.svg',
|
||||||
|
'./assets/img/user-icons/male-3.svg',
|
||||||
|
'./assets/img/user-icons/male-4.svg',
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(public loginService: loginService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles file selection changes and initiates the upload of the avatar.
|
||||||
|
* @param event The DOM event containing the file selection.
|
||||||
|
*/
|
||||||
|
onFileChange(event: Event) {
|
||||||
|
const inputElement = event.target as HTMLInputElement;
|
||||||
|
|
||||||
|
if (inputElement.files && inputElement.files[0]) {
|
||||||
|
const file = inputElement.files[0];
|
||||||
|
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
console.error('Datei ist kein Bild.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectedFile = file;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e: any) => {
|
||||||
|
this.avatarSrc = e.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
this.uploadFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads the selected file to Firebase Storage.
|
||||||
|
* @param file The file to be uploaded.
|
||||||
|
*/
|
||||||
|
uploadFile(file: File) {
|
||||||
|
const storage = getStorage();
|
||||||
|
const storageRef = ref(storage, 'avatars/' + file.name);
|
||||||
|
uploadBytes(storageRef, file)
|
||||||
|
.then((snapshot) => {
|
||||||
|
getDownloadURL(ref(storage, 'avatars/' + file.name))
|
||||||
|
.then((url) => {
|
||||||
|
this.avatarSrc = url;
|
||||||
|
this.loginService.getAvatarUrl(url);
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
console.error('Fehler beim Abrufen der Download-URL:', error)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Fehler beim Hochladen:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects an avatar from the list of predefined avatars.
|
||||||
|
* @param index Index of the chosen avatar in the avatarImages list.
|
||||||
|
*/
|
||||||
|
chooseExistAvatar(index: number) {
|
||||||
|
this.avatarSrc = this.avatarImages[index];
|
||||||
|
this.loginService.getAvatarUrl(this.avatarSrc);
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/app/components/main-login/login/login.component.html
Normal file
102
src/app/components/main-login/login/login.component.html
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
<div class="loginSektion startIntroScrollProtect">
|
||||||
|
<app-start-header></app-start-header>
|
||||||
|
<div class="loginContainer">
|
||||||
|
<div class="loginTop">
|
||||||
|
<span>{{ "login.header" | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="loginInfoText">
|
||||||
|
<span>{{ "login.infoText" | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<form (ngSubmit)="onSubmit()" #contactForm="ngForm" class="contactForm">
|
||||||
|
<div class="inputContainer">
|
||||||
|
<img class="iconP" src="./assets/img/login/mail.png" alt="" />
|
||||||
|
<input
|
||||||
|
class="inputField"
|
||||||
|
#emailField="ngModel"
|
||||||
|
[(ngModel)]="loginService.email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="{{ 'login.inputField' | translate }}"
|
||||||
|
required
|
||||||
|
[ngClass]="{ aktivInput: emailField.valid && emailField.touched }"
|
||||||
|
pattern="[^@]+@[^.]+\..+"
|
||||||
|
/>
|
||||||
|
@if (!emailField.valid && emailField.touched) {
|
||||||
|
<p class="error">{{ "login.errorText" | translate }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<img class="iconP" src="./assets/img/login/lock.png" alt="" />
|
||||||
|
<input
|
||||||
|
class="inputField"
|
||||||
|
#passwordField="ngModel"
|
||||||
|
minlength="6"
|
||||||
|
maxlength="16"
|
||||||
|
[(ngModel)]="this.loginService.password"
|
||||||
|
name="password"
|
||||||
|
[type]="loginService.passwordFieldType"
|
||||||
|
placeholder="{{ 'login.inputField2' | translate }}"
|
||||||
|
id="message"
|
||||||
|
required
|
||||||
|
[ngClass]="{
|
||||||
|
aktivInput: passwordField.valid && passwordField.touched
|
||||||
|
}"
|
||||||
|
cols="30"
|
||||||
|
rows="10"
|
||||||
|
/>
|
||||||
|
<img class="passwordEye" (click)="loginService.togglePasswordVisibility()" [src]="loginService.passwordIcon" alt="Toggle visibility">
|
||||||
|
@if (!passwordField.valid && passwordField.touched) {
|
||||||
|
<p class="error">{{ "login.errorText2" | translate }}</p>
|
||||||
|
} @if (loginService.errorMessage) {
|
||||||
|
<p class="error">{{ loginService.errorMessage }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="passwordContainer">
|
||||||
|
<span routerLink="/passwordForget">{{
|
||||||
|
"login.midText" | translate
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="separatorContainer">
|
||||||
|
<div></div>
|
||||||
|
<span>{{ "login.midSeperator" | translate }}</span>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div (click)="loginService.googleLogin()" class="googleLogingContainer">
|
||||||
|
<img src="./assets/img/login/google.svg" alt="" />
|
||||||
|
<span>{{ "login.googleBtn" | translate }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="loginBottom">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
[ngClass]="{ aktivButton: contactForm.valid }"
|
||||||
|
[disabled]="!contactForm.valid"
|
||||||
|
>
|
||||||
|
{{ "login.BtnLeft" | translate }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="guestButton"
|
||||||
|
(click)="loginService.guestLogin()"
|
||||||
|
>
|
||||||
|
{{ "login.BtnRight" | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="mobileRegisterContainer">
|
||||||
|
<div>
|
||||||
|
<span> {{ "login.topText" | translate }}</span>
|
||||||
|
<div class="mobileCreateAccountContainer">
|
||||||
|
<span routerLink="/register" class="mobileCreateAccoun">
|
||||||
|
{{ "login.topText2" | translate }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-footer></app-footer>
|
||||||
|
</div>
|
||||||
249
src/app/components/main-login/login/login.component.scss
Normal file
249
src/app/components/main-login/login/login.component.scss
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
@import "../../../shared/components/login/mixins/mixin.scss";
|
||||||
|
.startIntroScrollProtect {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
.loginSektion {
|
||||||
|
@include SektionBody;
|
||||||
|
}
|
||||||
|
.loginContainer {
|
||||||
|
@include Card;
|
||||||
|
}
|
||||||
|
.loginTop {
|
||||||
|
@include topText;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginBackImg {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.loginInfoText {
|
||||||
|
@include InfoText;
|
||||||
|
}
|
||||||
|
.inputContainer {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.iconP {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
.inputField {
|
||||||
|
@include inputStyle;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
@include errorStyle;
|
||||||
|
}
|
||||||
|
.contactForm {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
.passwordContainer {
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(121, 126, 243, 1);
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
margin-top: 40px;
|
||||||
|
> span {
|
||||||
|
font-family: Figtree, sans-serif, Nunito;
|
||||||
|
color: #797ef3;
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: #eceefe;
|
||||||
|
padding: 12px 16px 12px 16px;
|
||||||
|
border-radius: 100px;
|
||||||
|
color: #444df2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.separatorContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
> div {
|
||||||
|
border: 1px solid #adb0d9;
|
||||||
|
width: 42%;
|
||||||
|
height: 0px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.googleLogingContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
border: 1px solid #ffff;
|
||||||
|
font-size: 18px;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #eceefe;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 100px;
|
||||||
|
color: rgba(121, 126, 243, 1);
|
||||||
|
&:hover {
|
||||||
|
font-weight: 700;
|
||||||
|
border: 1px solid #444df2;
|
||||||
|
color: #444df2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginBottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 32px;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@include buttonGrey;
|
||||||
|
}
|
||||||
|
.aktivButton {
|
||||||
|
@include aktivButtonBlue;
|
||||||
|
}
|
||||||
|
.aktivInput {
|
||||||
|
border: 1px solid#686868;
|
||||||
|
}
|
||||||
|
.guestButton {
|
||||||
|
padding: 12px 25px;
|
||||||
|
color: #444df2;
|
||||||
|
background-color: #ffff;
|
||||||
|
border: 1px solid #444df2;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: 0.2s;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: #444df2;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #ffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mobileRegisterContainer {
|
||||||
|
display: none;
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
text-align: center;
|
||||||
|
> span {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mobileCreateAccountContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
.mobileCreateAccoun {
|
||||||
|
font-family: Figtree, sans-serif, Nunito;
|
||||||
|
color: #797ef3;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 12px 4px 12px;
|
||||||
|
border-radius: 100px;
|
||||||
|
border: 1px solid #eceefe;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 18px;
|
||||||
|
&:hover {
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: #eceefe;
|
||||||
|
color: #444df2;
|
||||||
|
border: 1px solid #444df2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.passwordEye {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 25px;
|
||||||
|
}
|
||||||
|
@media (max-width: 570px) {
|
||||||
|
.loginTop {
|
||||||
|
margin-bottom: 0;
|
||||||
|
> span {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loginInfoText {
|
||||||
|
margin-bottom: 0;
|
||||||
|
> span {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loginContainer {
|
||||||
|
padding: 16px;
|
||||||
|
max-height: 552px;
|
||||||
|
padding-left: 18px;
|
||||||
|
padding-right: 18px;
|
||||||
|
gap: 12px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.contactForm {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.inputContainer {
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
.inputField {
|
||||||
|
height: 52px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.iconP {
|
||||||
|
top: 15px;
|
||||||
|
}
|
||||||
|
.passwordContainer {
|
||||||
|
margin-bottom: 22px;
|
||||||
|
margin-top: 32px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
.separatorContainer {
|
||||||
|
font-size: 17px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
.googleLogingContainer {
|
||||||
|
font-size: 17px;
|
||||||
|
height: 52px;
|
||||||
|
gap: 0;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
> img {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loginBottom {
|
||||||
|
margin-top: 0px;
|
||||||
|
gap: 24px;
|
||||||
|
> button {
|
||||||
|
width: 118px;
|
||||||
|
height: 52px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.guestButton {
|
||||||
|
font-size: 17px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.aktivButton {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
.mobileRegisterContainer {
|
||||||
|
display: flex;
|
||||||
|
background-color: #eceefe;
|
||||||
|
width: 100vw;
|
||||||
|
padding-top: 40px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 380px) {
|
||||||
|
.loginContainer {
|
||||||
|
max-width: 306px;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/app/components/main-login/login/login.component.spec.ts
Normal file
23
src/app/components/main-login/login/login.component.spec.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoginComponent } from './login.component';
|
||||||
|
|
||||||
|
describe('LoginComponent', () => {
|
||||||
|
let component: LoginComponent;
|
||||||
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LoginComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LoginComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
62
src/app/components/main-login/login/login.component.ts
Normal file
62
src/app/components/main-login/login/login.component.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { Component, ElementRef, Renderer2 } from '@angular/core';
|
||||||
|
import { loginService } from '../../../service/login.service';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { FooterComponent } from '../../../shared/components/login/footer/footer.component';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { StartHeaderComponent } from '../../../shared/components/login/start-header/start-header.component';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './login.component.html',
|
||||||
|
styleUrl: './login.component.scss',
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
CommonModule,
|
||||||
|
FooterComponent,
|
||||||
|
RouterLink,
|
||||||
|
StartHeaderComponent,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class LoginComponent {
|
||||||
|
constructor(
|
||||||
|
public loginService: loginService,
|
||||||
|
private elRef: ElementRef,
|
||||||
|
private renderer: Renderer2
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the component and manages the removal of the 'startIntroScrollProtect' class from an element.
|
||||||
|
* On the first load, this class is removed after a delay of 4.5 seconds to manage initial UI transitions.
|
||||||
|
* On subsequent loads, the class is removed immediately, ensuring the UI remains consistent without delay.
|
||||||
|
* This behavior is controlled by the `isFirstLoad` property of the loginService.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
const element = this.elRef.nativeElement.querySelector(
|
||||||
|
'.startIntroScrollProtect'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.loginService.isFirstLoad) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (element) {
|
||||||
|
this.renderer.removeClass(element, 'startIntroScrollProtect');
|
||||||
|
}
|
||||||
|
}, 4500);
|
||||||
|
|
||||||
|
this.loginService.isFirstLoad = false;
|
||||||
|
} else if (element) {
|
||||||
|
this.renderer.removeClass(element, 'startIntroScrollProtect');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles form submission by triggering the login process through a login service.
|
||||||
|
* This method would typically be called when a user submits a form associated with logging in.
|
||||||
|
*/
|
||||||
|
onSubmit() {
|
||||||
|
this.loginService.login();
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/app/components/main-login/main-login.component.html
Normal file
5
src/app/components/main-login/main-login.component.html
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<app-login></app-login>
|
||||||
|
<app-register></app-register>
|
||||||
|
<app-choose-avatar ></app-choose-avatar>
|
||||||
|
<app-password-forget></app-password-forget>
|
||||||
|
<app-password-reset></app-password-reset>
|
||||||
3
src/app/components/main-login/main-login.component.scss
Normal file
3
src/app/components/main-login/main-login.component.scss
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
23
src/app/components/main-login/main-login.component.spec.ts
Normal file
23
src/app/components/main-login/main-login.component.spec.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MainLoginComponent } from './main-login.component';
|
||||||
|
|
||||||
|
describe('LoginComponent', () => {
|
||||||
|
let component: MainLoginComponent;
|
||||||
|
let fixture: ComponentFixture<MainLoginComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MainLoginComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(MainLoginComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
20
src/app/components/main-login/main-login.component.ts
Normal file
20
src/app/components/main-login/main-login.component.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { RegisterComponent } from "./register/register.component";
|
||||||
|
import { ChooseAvatarComponent } from "./choose-avatar/choose-avatar.component";
|
||||||
|
import { LoginComponent } from "./login/login.component";
|
||||||
|
import { PasswordForgetComponent } from "./password-forget/password-forget.component";
|
||||||
|
import { PasswordResetComponent } from "./password-reset/password-reset.component";
|
||||||
|
import { loginService } from '../../service/login.service';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-main-login',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './main-login.component.html',
|
||||||
|
styleUrl: './main-login.component.scss',
|
||||||
|
imports: [CommonModule,RegisterComponent, ChooseAvatarComponent, LoginComponent, PasswordForgetComponent, PasswordResetComponent,RouterLink]
|
||||||
|
})
|
||||||
|
export class MainLoginComponent {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<div class="passwordSektion">
|
||||||
|
<app-start-header [display]="'none'"></app-start-header>
|
||||||
|
<div class="passwordContainer">
|
||||||
|
<div class="passwordTop">
|
||||||
|
<div [routerLink]="['/login']" class="loginBackImg">
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'login/arrow-back.svg'"
|
||||||
|
[imgSize]="'24px'"
|
||||||
|
[btnSize]="'48px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
>
|
||||||
|
</app-small-btn>
|
||||||
|
</div>
|
||||||
|
<span>{{ 'passwordForget.header' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="passwordInfoText">
|
||||||
|
<span>{{ 'passwordForget.infoText' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
(ngSubmit)="onSubmit(contactForm)"
|
||||||
|
#contactForm="ngForm"
|
||||||
|
class="contactForm"
|
||||||
|
>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<img class="iconP" src="./assets/img/login/mail.png" alt="" />
|
||||||
|
<input
|
||||||
|
class="inputField"
|
||||||
|
#emailField="ngModel"
|
||||||
|
[(ngModel)]="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="{{ 'passwordForget.inputfield' | translate }}"
|
||||||
|
required
|
||||||
|
[ngClass]="{ aktivInput: emailField.valid && emailField.touched }"
|
||||||
|
pattern="[^@]+@[^.]+\..+"
|
||||||
|
/>
|
||||||
|
@if (!emailField.valid && emailField.touched) {
|
||||||
|
<p class="error">{{ 'login.errorText' | translate }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="bottomInfoText">
|
||||||
|
<span>
|
||||||
|
{{ 'passwordForget.infoTextBottom' | translate }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="passwordBottom">
|
||||||
|
<button
|
||||||
|
[ngClass]="{ aktivButton: contactForm.valid }"
|
||||||
|
[disabled]="!contactForm.valid"
|
||||||
|
>
|
||||||
|
{{ 'passwordForget.Btn' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="sendBtn" [style.display]="emailSentBtn ? 'block' : 'none'">
|
||||||
|
<img src="./assets/img/login/email-send-btn.svg" alt="" />
|
||||||
|
</div>
|
||||||
|
<app-footer></app-footer>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
@import "../../../shared/components/login/mixins/mixin.scss";
|
||||||
|
|
||||||
|
.passwordSektion {
|
||||||
|
@include SektionBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordContainer {
|
||||||
|
max-width: 606px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
gap: 24px;
|
||||||
|
max-height: 470px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordTop {
|
||||||
|
@include topText;
|
||||||
|
> span {
|
||||||
|
width: 90%;
|
||||||
|
padding-right: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordBackImg {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordInfoText {
|
||||||
|
@include InfoText;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconP {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField {
|
||||||
|
@include inputStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
@include errorStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactForm {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordBottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
margin-top: 32px;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@include buttonGrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aktivButton {
|
||||||
|
@include aktivButtonBlue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aktivInput {
|
||||||
|
border: 1px solid#686868;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomInfoText {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
> span {
|
||||||
|
width: 380px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #686868;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sendBtn {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 11%;
|
||||||
|
animation: slideInOut 4s ease-in-out forwards;
|
||||||
|
> img {
|
||||||
|
height: 118px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInOut {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
10%,
|
||||||
|
90% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 570px) {
|
||||||
|
.passwordTop {
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.passwordInfoText {
|
||||||
|
> span {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.inputField {
|
||||||
|
padding-left: 40px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
.iconP {
|
||||||
|
left: 12px;
|
||||||
|
}
|
||||||
|
.bottomInfoText {
|
||||||
|
> span {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.passwordTop {
|
||||||
|
> span {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contactForm {
|
||||||
|
min-width: 262px;
|
||||||
|
}
|
||||||
|
.passwordContainer {
|
||||||
|
gap: 16px;
|
||||||
|
max-width: 306px;
|
||||||
|
}
|
||||||
|
.inputField {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.bottomInfoText {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.inputContainer {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PasswordForgetComponent } from './password-forget.component';
|
||||||
|
|
||||||
|
describe('PasswordForgetComponent', () => {
|
||||||
|
let component: PasswordForgetComponent;
|
||||||
|
let fixture: ComponentFixture<PasswordForgetComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PasswordForgetComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PasswordForgetComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { FooterComponent } from '../../../shared/components/login/footer/footer.component';
|
||||||
|
import { FormsModule, NgForm } from '@angular/forms';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { SmallBtnComponent } from '../../../shared/components/small-btn/small-btn.component';
|
||||||
|
import { getAuth, sendPasswordResetEmail } from 'firebase/auth';
|
||||||
|
import { Firestore } from '@angular/fire/firestore';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { StartHeaderComponent } from '../../../shared/components/login/start-header/start-header.component';
|
||||||
|
import { TranslateModule} from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-password-forget',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './password-forget.component.html',
|
||||||
|
styleUrl: './password-forget.component.scss',
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
CommonModule,
|
||||||
|
FooterComponent,
|
||||||
|
RouterModule,
|
||||||
|
SmallBtnComponent,
|
||||||
|
StartHeaderComponent,
|
||||||
|
TranslateModule
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export class PasswordForgetComponent {
|
||||||
|
email: string = '';
|
||||||
|
emailSentBtn = false;
|
||||||
|
firestore: Firestore = inject(Firestore);
|
||||||
|
constructor(private router: Router) {}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates a password reset process for the user.
|
||||||
|
* Sends a password reset email to the user's registered email address.
|
||||||
|
*
|
||||||
|
* @param ngForm The form data from the Angular form, used to manage form state.
|
||||||
|
*/
|
||||||
|
passwordReset(ngForm: NgForm) {
|
||||||
|
const auth = getAuth();
|
||||||
|
sendPasswordResetEmail(auth, this.email)
|
||||||
|
.then(() => {
|
||||||
|
ngForm.resetForm(ngForm);
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorCode = error.code;
|
||||||
|
const errorMessage = error.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the form submission, invoking the password reset function and then sending a notification email.
|
||||||
|
*
|
||||||
|
* @param ngForm The form instance containing user input.
|
||||||
|
*/
|
||||||
|
onSubmit(ngForm: NgForm) {
|
||||||
|
this.passwordReset(ngForm);
|
||||||
|
this.sendEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulates sending an email by toggling a button state to show feedback to the user.
|
||||||
|
* After a set timeout, resets the button state.
|
||||||
|
*/
|
||||||
|
sendEmail() {
|
||||||
|
this.emailSentBtn = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.emailSentBtn = false;
|
||||||
|
}, 6000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
<div class="passwordSektion">
|
||||||
|
<app-start-header [display]="'none'"></app-start-header>
|
||||||
|
<div class="passwordContainer">
|
||||||
|
<div class="passwordTop">
|
||||||
|
<div [routerLink]="['/login']" class="loginBackImg">
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'login/arrow-back.svg'"
|
||||||
|
[imgSize]="'24px'"
|
||||||
|
[btnSize]="'48px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
>
|
||||||
|
</app-small-btn>
|
||||||
|
</div>
|
||||||
|
<span>{{ 'passwordReset.header' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
(ngSubmit)="onSubmit(contactForm)"
|
||||||
|
#contactForm="ngForm"
|
||||||
|
class="contactForm"
|
||||||
|
>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<img class="iconP" src="./assets/img/login/lock.png" alt="" />
|
||||||
|
<input
|
||||||
|
class="inputField"
|
||||||
|
#passwordField="ngModel"
|
||||||
|
minlength="6"
|
||||||
|
[(ngModel)]="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="{{ 'passwordReset.inputfield' | translate }}"
|
||||||
|
type="password"
|
||||||
|
id="message"
|
||||||
|
required
|
||||||
|
[ngClass]="{
|
||||||
|
aktivInput: passwordField.valid && passwordField.touched
|
||||||
|
}"
|
||||||
|
cols="30"
|
||||||
|
rows="10"
|
||||||
|
/>
|
||||||
|
@if (!passwordField.valid && passwordField.touched) {
|
||||||
|
<p class="error">
|
||||||
|
{{ 'passwordReset.errorText' | translate }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<img class="iconP" src="./assets/img/login/lock.png" alt="" />
|
||||||
|
<input
|
||||||
|
class="inputField"
|
||||||
|
#passwordRepeatField="ngModel"
|
||||||
|
minlength="6"
|
||||||
|
[(ngModel)]="passwordRepeat"
|
||||||
|
name="passwordRepeat"
|
||||||
|
placeholder="{{ 'passwordReset.inputfield2' | translate }}"
|
||||||
|
id="message"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
[ngClass]="{
|
||||||
|
aktivInput: passwordRepeatField.valid && passwordRepeatField.touched
|
||||||
|
}"
|
||||||
|
cols="30"
|
||||||
|
rows="10"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="error"
|
||||||
|
*ngIf="passwordRepeatField.touched && !passwordsMatch()"
|
||||||
|
>
|
||||||
|
{{ 'passwordReset.errorText2' | translate }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="passwordBottom">
|
||||||
|
<button
|
||||||
|
[ngClass]="{ aktivButton: contactForm.valid && passwordsMatch() }"
|
||||||
|
[disabled]="!contactForm.valid || !passwordsMatch()"
|
||||||
|
>
|
||||||
|
{{ 'passwordReset.Btn' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<app-footer></app-footer>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
@import "../../../shared/components/login/mixins/mixin.scss";
|
||||||
|
.passwordSektion {
|
||||||
|
@include SektionBody;
|
||||||
|
}
|
||||||
|
.passwordContainer {
|
||||||
|
max-width: 606px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
gap: 24px;
|
||||||
|
max-height: 470px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.passwordTop {
|
||||||
|
@include topText;
|
||||||
|
> span {
|
||||||
|
width: 90%;
|
||||||
|
padding-right: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordBackImg {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
.iconP {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
.inputField {
|
||||||
|
@include inputStyle;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
@include errorStyle;
|
||||||
|
}
|
||||||
|
.contactForm {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordBottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
margin-top: 32px;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@include buttonGrey;
|
||||||
|
}
|
||||||
|
.aktivButton {
|
||||||
|
@include aktivButtonBlue;
|
||||||
|
}
|
||||||
|
.aktivInput {
|
||||||
|
border: 1px solid#686868;
|
||||||
|
}
|
||||||
|
@media (max-width: 570px) {
|
||||||
|
.passwordTop {
|
||||||
|
> span {
|
||||||
|
font-size: 38px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 570px) {
|
||||||
|
.passwordTop {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font-size: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.inputField {
|
||||||
|
padding-left: 40px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
.iconP {
|
||||||
|
left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.passwordTop {
|
||||||
|
> span {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contactForm {
|
||||||
|
min-width: 262px;
|
||||||
|
}
|
||||||
|
.passwordContainer {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.inputField {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.bottomInfoText {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.inputContainer {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PasswordResetComponent } from './password-reset.component';
|
||||||
|
|
||||||
|
describe('PasswordResetComponent', () => {
|
||||||
|
let component: PasswordResetComponent;
|
||||||
|
let fixture: ComponentFixture<PasswordResetComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PasswordResetComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PasswordResetComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { FooterComponent } from '../../../shared/components/login/footer/footer.component';
|
||||||
|
import { FormsModule, NgForm } from '@angular/forms';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { StartHeaderComponent } from '../../../shared/components/login/start-header/start-header.component';
|
||||||
|
import { SmallBtnComponent } from '../../../shared/components/small-btn/small-btn.component';
|
||||||
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
|
import { getAuth } from 'firebase/auth';
|
||||||
|
import { confirmPasswordReset } from 'firebase/auth';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-password-reset',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
CommonModule,
|
||||||
|
StartHeaderComponent,
|
||||||
|
FooterComponent,
|
||||||
|
RouterModule,
|
||||||
|
SmallBtnComponent,
|
||||||
|
TranslateModule
|
||||||
|
],
|
||||||
|
templateUrl: './password-reset.component.html',
|
||||||
|
styleUrl: './password-reset.component.scss',
|
||||||
|
})
|
||||||
|
export class PasswordResetComponent {
|
||||||
|
password: string = '';
|
||||||
|
passwordRepeat: string = '';
|
||||||
|
oobCode: string = '';
|
||||||
|
private queryParamsSubscription: Subscription = new Subscription();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the component by subscribing to query parameters.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.queryParamsSubscription = this.route.queryParams.subscribe(
|
||||||
|
(params) => {
|
||||||
|
this.oobCode = params['oobCode'];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the submission of the password reset form.
|
||||||
|
* @param ngForm The form data of the password reset form.
|
||||||
|
*/
|
||||||
|
onSubmit(ngForm: NgForm): void {
|
||||||
|
this.resetPassword(ngForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the password reset operation using Firebase Authentication.
|
||||||
|
* @param ngForm The form data of the password reset form.
|
||||||
|
*/
|
||||||
|
resetPassword(ngForm: NgForm): void {
|
||||||
|
const auth = getAuth();
|
||||||
|
const newPassword = this.passwordRepeat;
|
||||||
|
confirmPasswordReset(auth, this.oobCode, newPassword)
|
||||||
|
.then(() => {
|
||||||
|
ngForm.resetForm();
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Fehler beim Zurücksetzen des Passworts:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up the component by unsubscribing from the query parameters subscription.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.queryParamsSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the entered passwords match.
|
||||||
|
* @returns true if the password and repeated password are the same.
|
||||||
|
*/
|
||||||
|
passwordsMatch(): boolean {
|
||||||
|
return this.password === this.passwordRepeat;
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/app/components/main-login/register/register.component.html
Normal file
117
src/app/components/main-login/register/register.component.html
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
<div class="registerSektion">
|
||||||
|
<app-start-header [display]="'none'"></app-start-header>
|
||||||
|
<div class="registerContainer">
|
||||||
|
<div class="registerTop">
|
||||||
|
<div routerLink="/login" class="registerBackImg">
|
||||||
|
<app-small-btn
|
||||||
|
[imgSrc]="'login/arrow-back.svg'"
|
||||||
|
[imgSize]="'24px'"
|
||||||
|
[btnSize]="'48px'"
|
||||||
|
[btnBgHoverColor]="'#edeefe'"
|
||||||
|
(click)="resetValues()"
|
||||||
|
>
|
||||||
|
</app-small-btn>
|
||||||
|
</div>
|
||||||
|
<span> {{ 'createAccount.header' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="registerInfoText">
|
||||||
|
<span
|
||||||
|
>{{ 'createAccount.infoText' | translate }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
(ngSubmit)="onSubmit(contactForm)"
|
||||||
|
#contactForm="ngForm"
|
||||||
|
class="contactForm"
|
||||||
|
>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<img class="iconP" src="./assets/img/login/person-filled.png" alt="" />
|
||||||
|
<input
|
||||||
|
class="inputField"
|
||||||
|
#nameField="ngModel"
|
||||||
|
[(ngModel)]="loginService.name"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
placeholder="{{ 'createAccount.inputField' | translate }}"
|
||||||
|
required
|
||||||
|
[ngClass]="{ aktivInput: nameField.valid && nameField.touched }"
|
||||||
|
/>
|
||||||
|
@if (!nameField.valid && nameField.touched) {
|
||||||
|
<p class="error">{{ 'createAccount.errorText' | translate }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<img class="iconP" src="./assets/img/login/mail.png" alt="" />
|
||||||
|
<input
|
||||||
|
class="inputField"
|
||||||
|
#emailField="ngModel"
|
||||||
|
[(ngModel)]="loginService.email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="{{ 'createAccount.inputField2' | translate }}"
|
||||||
|
required
|
||||||
|
[ngClass]="{ aktivInput: emailField.valid && emailField.touched }"
|
||||||
|
pattern="[^@]+@[^.]+\..+"
|
||||||
|
/>
|
||||||
|
@if (!emailField.valid && emailField.touched) {
|
||||||
|
<p class="error">{{ 'createAccount.errorText2' | translate }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<img class="iconP" src="./assets/img/login/lock.png" alt="" />
|
||||||
|
<input
|
||||||
|
class="inputField"
|
||||||
|
#passwordField="ngModel"
|
||||||
|
minlength="6"
|
||||||
|
maxlength="16"
|
||||||
|
[(ngModel)]="loginService.password"
|
||||||
|
name="password"
|
||||||
|
placeholder="{{ 'createAccount.inputField3' | translate }}"
|
||||||
|
[type]="loginService.passwordFieldType"
|
||||||
|
id="message"
|
||||||
|
required
|
||||||
|
[ngClass]="{
|
||||||
|
aktivInput: passwordField.valid && passwordField.touched
|
||||||
|
}"
|
||||||
|
cols="30"
|
||||||
|
rows="10"
|
||||||
|
/>
|
||||||
|
<img class="passwordEye" (click)="loginService.togglePasswordVisibility()" [src]="loginService.passwordIcon" alt="Toggle visibility">
|
||||||
|
@if (!passwordField.valid && passwordField.touched) {
|
||||||
|
<p class="error">
|
||||||
|
{{ 'createAccount.errorText3' | translate }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkBox">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
[src]="currentImage"
|
||||||
|
(click)="toggleCheckbox()"
|
||||||
|
(mouseover)="onMouseOver()"
|
||||||
|
(mouseout)="onMouseOut()"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
>{{ 'createAccount.midText' | translate }}
|
||||||
|
<span class="fontColor" routerLink="/privacy-policy"
|
||||||
|
>{{ 'createAccount.midText2' | translate }} </span
|
||||||
|
>{{ 'createAccount.midText3' | translate }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="registerBottom">
|
||||||
|
<button
|
||||||
|
[ngClass]="{ aktivButton: isChecked && contactForm.valid }"
|
||||||
|
[disabled]="!isChecked || !contactForm.valid"
|
||||||
|
>
|
||||||
|
{{ 'createAccount.Btn' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<app-footer></app-footer>
|
||||||
|
</div>
|
||||||
135
src/app/components/main-login/register/register.component.scss
Normal file
135
src/app/components/main-login/register/register.component.scss
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
@import "../../../shared/components/login/mixins/mixin.scss";
|
||||||
|
.registerSektion {
|
||||||
|
@include SektionBody;
|
||||||
|
}
|
||||||
|
.registerContainer {
|
||||||
|
@include Card;
|
||||||
|
gap: 42px !important;
|
||||||
|
}
|
||||||
|
.registerTop {
|
||||||
|
@include topText;
|
||||||
|
> span {
|
||||||
|
width: 90%;
|
||||||
|
padding-right: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerBackImg {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.registerInfoText {
|
||||||
|
@include InfoText;
|
||||||
|
}
|
||||||
|
.inputContainer {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.iconP {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
.inputField {
|
||||||
|
@include inputStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
@include errorStyle;
|
||||||
|
}
|
||||||
|
.contactForm {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
.checkBox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
> div {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.registerBottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@include buttonGrey;
|
||||||
|
}
|
||||||
|
.aktivButton {
|
||||||
|
@include aktivButtonBlue;
|
||||||
|
}
|
||||||
|
.aktivInput {
|
||||||
|
border: 1px solid#686868;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fontColor {
|
||||||
|
color: #797ef3;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: #eceefe;
|
||||||
|
padding: 2px 2px 2px 5px;
|
||||||
|
border-radius: 100px;
|
||||||
|
color: #444df2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.passwordEye {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 570px) {
|
||||||
|
.registerTop {
|
||||||
|
margin-bottom: 0;
|
||||||
|
> span {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.registerInfoText {
|
||||||
|
margin-bottom: 0;
|
||||||
|
> span {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.registerContainer {
|
||||||
|
padding: 16px;
|
||||||
|
max-height: 552px;
|
||||||
|
padding-left: 18px;
|
||||||
|
padding-right: 18px;
|
||||||
|
gap: 12px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.contactForm {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.inputContainer {
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
.inputField {
|
||||||
|
height: 52px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.iconP {
|
||||||
|
top: 15px;
|
||||||
|
}
|
||||||
|
.registerBottom {
|
||||||
|
margin-top: 26px;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.guestButton {
|
||||||
|
font-size: 17px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.aktivButton {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 380px) {
|
||||||
|
.registerContainer {
|
||||||
|
max-width: 306px;
|
||||||
|
gap: 12px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { RegisterComponent } from './register.component';
|
||||||
|
|
||||||
|
describe('RegisterComponent', () => {
|
||||||
|
let component: RegisterComponent;
|
||||||
|
let fixture: ComponentFixture<RegisterComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [RegisterComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(RegisterComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
95
src/app/components/main-login/register/register.component.ts
Normal file
95
src/app/components/main-login/register/register.component.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { FormsModule, NgForm } from '@angular/forms';
|
||||||
|
import { Firestore } from '@angular/fire/firestore';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterLink, Router } from '@angular/router';
|
||||||
|
import { SmallBtnComponent } from '../../../shared/components/small-btn/small-btn.component';
|
||||||
|
import { FooterComponent } from '../../../shared/components/login/footer/footer.component';
|
||||||
|
import { loginService } from '../../../service/login.service';
|
||||||
|
import { StartHeaderComponent } from '../../../shared/components/login/start-header/start-header.component';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-register',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './register.component.html',
|
||||||
|
styleUrl: './register.component.scss',
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
CommonModule,
|
||||||
|
SmallBtnComponent,
|
||||||
|
FooterComponent,
|
||||||
|
RouterLink,
|
||||||
|
StartHeaderComponent,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class RegisterComponent {
|
||||||
|
firestore: Firestore = inject(Firestore);
|
||||||
|
isChecked: boolean = false;
|
||||||
|
currentImage: string;
|
||||||
|
defaultImage = './assets/img/login/box.png';
|
||||||
|
clickedImage = './assets/img/login/box-checked.png';
|
||||||
|
hoverImage = './assets/img/login/box-hover.png';
|
||||||
|
clickedHoverImage = './assets/img/login/box-checked-hover.png';
|
||||||
|
|
||||||
|
constructor(public loginService: loginService, private router: Router) {
|
||||||
|
this.currentImage = this.defaultImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the form submission, extracting first and last names from a full name and navigating to the avatar page.
|
||||||
|
* @param ngForm The Angular form object representing the form submission.
|
||||||
|
*/
|
||||||
|
onSubmit(ngForm: NgForm) {
|
||||||
|
const names = this.loginService.name.split(' ');
|
||||||
|
this.loginService.firstName = names[0];
|
||||||
|
this.loginService.lastName = names.slice(1).join(' ');
|
||||||
|
this.router.navigate(['/avatar']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles a checkbox state and updates the corresponding image based on the new state.
|
||||||
|
*/
|
||||||
|
toggleCheckbox() {
|
||||||
|
this.isChecked = !this.isChecked;
|
||||||
|
this.updateImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the current image to a hover state image when the mouse is over the related component.
|
||||||
|
*/
|
||||||
|
onMouseOver() {
|
||||||
|
this.updateImage(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverts the current image to its default state when the mouse is out of the related component.
|
||||||
|
*/
|
||||||
|
onMouseOut() {
|
||||||
|
this.updateImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the image displayed based on the checkbox state and hover state.
|
||||||
|
* @param isHovering Boolean indicating if the mouse is hovering over the image component (default is false).
|
||||||
|
*/
|
||||||
|
updateImage(isHovering: boolean = false) {
|
||||||
|
if (this.isChecked) {
|
||||||
|
this.currentImage = isHovering
|
||||||
|
? this.clickedHoverImage
|
||||||
|
: this.clickedImage;
|
||||||
|
} else {
|
||||||
|
this.currentImage = isHovering ? this.hoverImage : this.defaultImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the values, if user click on the arrow back button.
|
||||||
|
*/
|
||||||
|
resetValues() {
|
||||||
|
this.loginService.name = '';
|
||||||
|
this.loginService.email = '';
|
||||||
|
this.loginService.password = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/app/components/main/main.component.html
Normal file
76
src/app/components/main/main.component.html
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<div (click)="toggleBooleans()">
|
||||||
|
@if (userService.isUserLogin) {
|
||||||
|
<app-header [viewWidth]="viewWidth"></app-header>
|
||||||
|
<div class="content">
|
||||||
|
<app-sidebar
|
||||||
|
[viewWidth]="viewWidth"
|
||||||
|
[btnPosition]="
|
||||||
|
this.viewWidth <= RESPONSIVE_THRESHOLD &&
|
||||||
|
!chatService.isSecondaryChatOpen &&
|
||||||
|
toggleAllBooleans.isSidebarOpen
|
||||||
|
"
|
||||||
|
[ngClass]="{
|
||||||
|
slideIn: this.viewWidth >= RESPONSIVE_THRESHOLD && toggleAllBooleans.isSidebarOpen,
|
||||||
|
slideOut: this.viewWidth >= RESPONSIVE_THRESHOLD && !toggleAllBooleans.isSidebarOpen,
|
||||||
|
}"
|
||||||
|
[ngStyle]="{
|
||||||
|
'margin-right':
|
||||||
|
this.viewWidth <= RESPONSIVE_THRESHOLD_MAX && chatService.isSecondaryChatOpen
|
||||||
|
? '-390px'
|
||||||
|
: '',
|
||||||
|
display:
|
||||||
|
this.viewWidth <= RESPONSIVE_THRESHOLD && !toggleAllBooleans.isSidebarOpen
|
||||||
|
? 'none'
|
||||||
|
: 'block',
|
||||||
|
|
||||||
|
}"
|
||||||
|
></app-sidebar>
|
||||||
|
<app-sidebar-toggle [viewWidth]="viewWidth"></app-sidebar-toggle>
|
||||||
|
<div
|
||||||
|
class="chatBox"
|
||||||
|
[ngStyle]="{
|
||||||
|
display:
|
||||||
|
this.viewWidth <= RESPONSIVE_THRESHOLD &&
|
||||||
|
!chatService.isSecondaryChatOpen &&
|
||||||
|
toggleAllBooleans.isSidebarOpen
|
||||||
|
? 'none'
|
||||||
|
: ''
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<app-main-chat
|
||||||
|
[isSecondaryChatOpen]="chatService.isSecondaryChatOpen"
|
||||||
|
[currentChannel]="currentChannel"
|
||||||
|
[viewWidth]="viewWidth"
|
||||||
|
[ngStyle]="{
|
||||||
|
display:
|
||||||
|
this.viewWidth <= RESPONSIVE_THRESHOLD && (chatService.isSecondaryChatOpen || toggleAllBooleans.isSidebarOpen)
|
||||||
|
? 'none'
|
||||||
|
: 'block',
|
||||||
|
width:
|
||||||
|
this.viewWidth <= RESPONSIVE_THRESHOLD && (chatService.isSecondaryChatOpen || toggleAllBooleans.isSidebarOpen)
|
||||||
|
? '0%'
|
||||||
|
: '',
|
||||||
|
}"
|
||||||
|
></app-main-chat>
|
||||||
|
<app-secondary-chat
|
||||||
|
[currentChannel]="currentChannel"
|
||||||
|
[viewWidth]="viewWidth"
|
||||||
|
[ngClass]="{
|
||||||
|
slideInRightWindow:
|
||||||
|
this.viewWidth >= RESPONSIVE_THRESHOLD && chatService.isSecondaryChatOpen,
|
||||||
|
slideOutRightWindow:
|
||||||
|
this.viewWidth >= RESPONSIVE_THRESHOLD && !chatService.isSecondaryChatOpen,
|
||||||
|
}"
|
||||||
|
[ngStyle]="{
|
||||||
|
display:
|
||||||
|
this.viewWidth <= RESPONSIVE_THRESHOLD &&
|
||||||
|
!chatService.isSecondaryChatOpen
|
||||||
|
? 'none'
|
||||||
|
: 'block'
|
||||||
|
}"
|
||||||
|
></app-secondary-chat>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<app-overlay></app-overlay>
|
||||||
|
</div>
|
||||||
129
src/app/components/main/main.component.scss
Normal file
129
src/app/components/main/main.component.scss
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
@import "../../../styles.scss";
|
||||||
|
|
||||||
|
app-header {
|
||||||
|
display: block;
|
||||||
|
height: 110px;
|
||||||
|
background-color: #edeefe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-sidebar,
|
||||||
|
app-main-chat,
|
||||||
|
app-secondary-chat {
|
||||||
|
padding: 0 12px 12px 12px;
|
||||||
|
min-height: calc(100vh - 122px);
|
||||||
|
background-color: #edeefe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatBox {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minusWidth {
|
||||||
|
width: calc(100% - 518px);
|
||||||
|
}
|
||||||
|
|
||||||
|
app-main-chat {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-sidebar {
|
||||||
|
max-width: 366px;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-secondary-chat {
|
||||||
|
max-width: 485px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slideIn {
|
||||||
|
float: right;
|
||||||
|
right: 0;
|
||||||
|
transition: 0.3s linear;
|
||||||
|
animation: slideIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slideOut {
|
||||||
|
float: left;
|
||||||
|
margin-right: -390px;
|
||||||
|
transition: 0.3s linear;
|
||||||
|
animation: slideOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slideInRightWindow {
|
||||||
|
transition: 0.3s linear;
|
||||||
|
width: 100%;
|
||||||
|
animation: slideIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
width: 100%;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slideOutRightWindow {
|
||||||
|
transition: 0.3s linear;
|
||||||
|
animation: slideOut;
|
||||||
|
width: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOut {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------- RESPONSIVE -------------*/
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD) {
|
||||||
|
app-sidebar-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-secondary-chat {
|
||||||
|
width: 100vw;
|
||||||
|
max-width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-sidebar {
|
||||||
|
width: 100vw;
|
||||||
|
max-width: 100vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE) {
|
||||||
|
app-header {
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-sidebar,
|
||||||
|
app-main-chat,
|
||||||
|
app-secondary-chat {
|
||||||
|
padding: 0;
|
||||||
|
min-height: calc(100vh - 80px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE380px) {
|
||||||
|
app-sidebar,
|
||||||
|
app-main-chat,
|
||||||
|
app-secondary-chat {
|
||||||
|
min-height: calc(100vh - 80px);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/app/components/main/main.component.spec.ts
Normal file
22
src/app/components/main/main.component.spec.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MainComponent } from './main.component';
|
||||||
|
|
||||||
|
describe('LandingPageComponent', () => {
|
||||||
|
let component: MainComponent;
|
||||||
|
let fixture: ComponentFixture<MainComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [MainComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(MainComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
114
src/app/components/main/main.component.ts
Normal file
114
src/app/components/main/main.component.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
import {
|
||||||
|
AfterViewChecked,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
HostListener,
|
||||||
|
OnChanges,
|
||||||
|
SimpleChanges,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { HeaderComponent } from '../../shared/components/header/header.component';
|
||||||
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||||
|
import { UserService } from '../../service/user.service';
|
||||||
|
import { SidebarComponent } from '../sidebar/sidebar.component';
|
||||||
|
import { MainChatComponent } from '../main-chat/main-chat.component';
|
||||||
|
import { SecondaryChatComponent } from '../secondary-chat/secondary-chat.component';
|
||||||
|
import { ChatService } from '../../service/chat.service';
|
||||||
|
import { ChannleService } from '../../service/channle.service';
|
||||||
|
import { SidebarToggleComponent } from '../sidebar/sidebar-toggle/sidebar-toggle.component';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { AddNewChannelComponent } from '../sidebar/sidebar-channels/add-new-channel/add-new-channel.component';
|
||||||
|
import { OverlayComponent } from '../../shared/components/overlay/overlay.component';
|
||||||
|
import { ToggleBooleanService } from '../../service/toggle-boolean.service';
|
||||||
|
import { SharedService } from '../../service/shared.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-landing-page',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
RouterModule,
|
||||||
|
HeaderComponent,
|
||||||
|
SidebarComponent,
|
||||||
|
MainChatComponent,
|
||||||
|
SecondaryChatComponent,
|
||||||
|
SidebarToggleComponent,
|
||||||
|
CommonModule,
|
||||||
|
AddNewChannelComponent,
|
||||||
|
OverlayComponent,
|
||||||
|
],
|
||||||
|
templateUrl: './main.component.html',
|
||||||
|
styleUrl: './main.component.scss',
|
||||||
|
})
|
||||||
|
export class MainComponent {
|
||||||
|
constructor(
|
||||||
|
public userService: UserService,
|
||||||
|
public chatService: ChatService,
|
||||||
|
public channelService: ChannleService,
|
||||||
|
private route: Router,
|
||||||
|
private router: ActivatedRoute,
|
||||||
|
private elementRef: ElementRef,
|
||||||
|
public toggleAllBooleans: ToggleBooleanService,
|
||||||
|
private sharedService: SharedService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
currentChannel: string = '';
|
||||||
|
viewWidth: number = 0;
|
||||||
|
|
||||||
|
RESPONSIVE_THRESHOLD = this.sharedService.RESPONSIVE_THRESHOLD;
|
||||||
|
RESPONSIVE_THRESHOLD_MAX = this.sharedService.RESPONSIVE_THRESHOLD_MAX;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.ifUserLogin();
|
||||||
|
this.routeUserId();
|
||||||
|
this.updateViewWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the user is logged in.
|
||||||
|
*/
|
||||||
|
ifUserLogin() {
|
||||||
|
if (this.userService.getCurrentUserId() === undefined) {
|
||||||
|
this.route.navigateByUrl('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to route params and updates the current channel.
|
||||||
|
*/
|
||||||
|
routeUserId() {
|
||||||
|
if (this.router.params.subscribe()) {
|
||||||
|
this.router.params.subscribe((params) => {
|
||||||
|
this.currentChannel = params['id'];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for window resize events.
|
||||||
|
*/
|
||||||
|
@HostListener('window:resize')
|
||||||
|
onResize() {
|
||||||
|
this.updateViewWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the view width based on the current window width.
|
||||||
|
*/
|
||||||
|
private updateViewWidth() {
|
||||||
|
this.viewWidth = window.innerWidth;
|
||||||
|
if (this.viewWidth <= this.RESPONSIVE_THRESHOLD) {
|
||||||
|
this.toggleAllBooleans.isSidebarOpen = false;
|
||||||
|
} else if (this.viewWidth >= this.RESPONSIVE_THRESHOLD) {
|
||||||
|
this.toggleAllBooleans.isSidebarOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles various boolean values to control UI elements.
|
||||||
|
*/
|
||||||
|
toggleBooleans() {
|
||||||
|
this.toggleAllBooleans.openSearchWindow = false;
|
||||||
|
this.toggleAllBooleans.openSearchWindowHead = false;
|
||||||
|
this.toggleAllBooleans.selectUserInMsgBox = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
<section>
|
||||||
|
<div class="chat">
|
||||||
|
<div class="header">
|
||||||
|
<div class="headline">
|
||||||
|
<p>{{ "secondary-chat.thread" | translate }}</p>
|
||||||
|
@for (channel of getChannelName(); track channel) {
|
||||||
|
<span># {{ channel.name }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="close" (click)="closeSecondaryChat()">
|
||||||
|
<img src="./assets/img/closeIcon.svg" alt="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content" #messageBody>
|
||||||
|
@for (chat of getSingleChat(this.chatService.isSecondaryChatId); track
|
||||||
|
chat; let i = $index) {
|
||||||
|
<div>
|
||||||
|
@for (user of getChatUsers(chat.userId); track user) {
|
||||||
|
<app-single-chat
|
||||||
|
[user]="user"
|
||||||
|
[chat]="chat"
|
||||||
|
[index]="i"
|
||||||
|
[currentChat]="chat.id"
|
||||||
|
[showAnswer]="false"
|
||||||
|
[openOnSecondaryChat]="true"
|
||||||
|
[secondaryChatFirstMsg]="true"
|
||||||
|
[viewWidth]="viewWidth"
|
||||||
|
></app-single-chat>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="spacer">
|
||||||
|
<p>
|
||||||
|
{{ displayCountChatAnswer(chat.id) }}
|
||||||
|
{{ "secondary-chat.answer" | translate }}
|
||||||
|
</p>
|
||||||
|
<div class="line"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="messages">
|
||||||
|
@for (chat of getChatAnswers(this.chatService.isSecondaryChatId); track
|
||||||
|
chat; let i = $index) {
|
||||||
|
<div>
|
||||||
|
@for (user of getChatUsers(chat.userId); track user) {
|
||||||
|
<app-single-chat
|
||||||
|
[user]="user"
|
||||||
|
[chat]="chat"
|
||||||
|
[index]="i"
|
||||||
|
[currentChat]="chat.id"
|
||||||
|
[showAnswer]="false"
|
||||||
|
[openOnSecondaryChat]="true"
|
||||||
|
[viewWidth]="viewWidth"
|
||||||
|
></app-single-chat>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="answer">
|
||||||
|
<app-chat-msg-box
|
||||||
|
[currentChannel]="currentChannel"
|
||||||
|
(newMsgEmitter)="editMsgEmitter($event)"
|
||||||
|
[target]="'chat-answers'"
|
||||||
|
></app-chat-msg-box>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
129
src/app/components/secondary-chat/secondary-chat.component.scss
Normal file
129
src/app/components/secondary-chat/secondary-chat.component.scss
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
@import "./../../../styles.scss";
|
||||||
|
|
||||||
|
section {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 24px;
|
||||||
|
background-color: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 40px;
|
||||||
|
padding: 24px 48px;
|
||||||
|
box-shadow: 0px 3px 3px 0px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: calc(100vh - 500px);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 28px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #797ef3;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: #edeefe;
|
||||||
|
border-radius: 100%;
|
||||||
|
img {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 48px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
p {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30%;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #adb0d9;
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
width: 70%;
|
||||||
|
margin: 24px 6px;
|
||||||
|
border: 1px solid #adb0d9;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mirror {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------- responsive ---------------
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE) {
|
||||||
|
section {
|
||||||
|
height: calc(100vh - 80px);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
height: 36px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: calc(100vh - 350px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
padding: 0 12px;
|
||||||
|
p {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
width: 70%;
|
||||||
|
margin: 24px 0px 24px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $RESPONSIVE_THRESHOLD_MOBILE380px) {
|
||||||
|
section {
|
||||||
|
height: calc(100vh - 80px);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
height: calc(100vh - 350px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SecondaryChatComponent } from './secondary-chat.component';
|
||||||
|
|
||||||
|
describe('SecondaryChatComponent', () => {
|
||||||
|
let component: SecondaryChatComponent;
|
||||||
|
let fixture: ComponentFixture<SecondaryChatComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SecondaryChatComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SecondaryChatComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
164
src/app/components/secondary-chat/secondary-chat.component.ts
Normal file
164
src/app/components/secondary-chat/secondary-chat.component.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
import {
|
||||||
|
AfterViewChecked,
|
||||||
|
AfterViewInit,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
Input,
|
||||||
|
Renderer2,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { UserService } from '../../service/user.service';
|
||||||
|
import { ChannleService } from '../../service/channle.service';
|
||||||
|
import { ChatService } from '../../service/chat.service';
|
||||||
|
import { Channel } from '../../interface/channel.interface';
|
||||||
|
import { MainComponent } from '../main/main.component';
|
||||||
|
import { SingleChatComponent } from '../main-chat/single-chat/single-chat.component';
|
||||||
|
import { Chat, ChatAnswers } from '../../interface/chat.interface';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { User } from '../../interface/user.interface';
|
||||||
|
import { ChatMsgBoxComponent } from '../main-chat/chat-msg-box/chat-msg-box.component';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-secondary-chat',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
MainComponent,
|
||||||
|
SingleChatComponent,
|
||||||
|
ChatMsgBoxComponent,
|
||||||
|
CommonModule,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
templateUrl: './secondary-chat.component.html',
|
||||||
|
styleUrl: './secondary-chat.component.scss',
|
||||||
|
})
|
||||||
|
export class SecondaryChatComponent implements AfterViewChecked {
|
||||||
|
@Input() currentChannel: string = '';
|
||||||
|
@Input() viewWidth: number = 0;
|
||||||
|
@ViewChild('messageBody') messageBody: ElementRef | undefined;
|
||||||
|
sidebarLoaded: boolean = false;
|
||||||
|
isNewMessage: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public userService: UserService,
|
||||||
|
public channelService: ChannleService,
|
||||||
|
public chatService: ChatService,
|
||||||
|
private renderer: Renderer2
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls to the bottom if the secondary chat is open and the sidebar has not been loaded yet.
|
||||||
|
*/
|
||||||
|
ngAfterViewChecked() {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.chatService.isSecondaryChatOpen && !this.sidebarLoaded) {
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the isNewMessage flag and scrolls to the bottom if there's a new message.
|
||||||
|
* @param {boolean} variable - Flag indicating if there's a new message.
|
||||||
|
*/
|
||||||
|
editMsgEmitter(variable: boolean) {
|
||||||
|
this.isNewMessage = variable;
|
||||||
|
if (this.isNewMessage) {
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls the message body to the bottom.
|
||||||
|
*/
|
||||||
|
scrollToBottom(): void {
|
||||||
|
if (this.messageBody) {
|
||||||
|
const element = this.messageBody.nativeElement;
|
||||||
|
this.renderer.setProperty(
|
||||||
|
element,
|
||||||
|
'scrollTop',
|
||||||
|
element.scrollHeight - element.clientHeight
|
||||||
|
);
|
||||||
|
this.sidebarLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the count of chat answers for a given chat ID.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @returns {number} - The count of chat answers.
|
||||||
|
*/
|
||||||
|
displayCountChatAnswer(chatId: string) {
|
||||||
|
return this.chatService.getChatAnswers(chatId).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the thread chat window.
|
||||||
|
*/
|
||||||
|
closeSecondaryChat() {
|
||||||
|
this.chatService.toggleSecondaryChat('none');
|
||||||
|
this.sidebarLoaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a single chat by its ID.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @returns {Chat[]} - An array containing the single chat.
|
||||||
|
*/
|
||||||
|
getSingleChat(chatId: string): Chat[] {
|
||||||
|
const filteredTasks = this.chatService.allChats.filter(
|
||||||
|
(chat) => chat.id == chatId
|
||||||
|
);
|
||||||
|
return filteredTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves chat answers for a given chat ID.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @returns {ChatAnswers[]} - An array containing chat answers.
|
||||||
|
*/
|
||||||
|
getChatAnswers(chatId: string): ChatAnswers[] {
|
||||||
|
const filteredTasks = this.chatService.allChatAnswers.filter(
|
||||||
|
(chat) => chat.chatId === chatId
|
||||||
|
);
|
||||||
|
|
||||||
|
filteredTasks.sort((a, b) => b.publishedTimestamp - a.publishedTimestamp);
|
||||||
|
|
||||||
|
return filteredTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves users belonging to a chat.
|
||||||
|
* @param {string} chatId - The ID of the chat.
|
||||||
|
* @returns {User[]} - An array containing users.
|
||||||
|
*/
|
||||||
|
getChatUsers(chatId: string) {
|
||||||
|
const filteredTasks = this.userService.allUsers.filter(
|
||||||
|
(user) => user.id == chatId
|
||||||
|
);
|
||||||
|
return filteredTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the name of the channel.
|
||||||
|
* @returns {Channel[]} - An array containing the channel name.
|
||||||
|
*/
|
||||||
|
getChannelName() {
|
||||||
|
const filteredTasks = this.channelService.allChannels.filter(
|
||||||
|
(channel) => channel.id == this.currentChannel
|
||||||
|
);
|
||||||
|
return filteredTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves chats belonging to a channel.
|
||||||
|
* @param {string} chatId - The ID of the chat (channel).
|
||||||
|
* @returns {Chat[]} - An array containing chats.
|
||||||
|
*/
|
||||||
|
getChatChannel(chatId: string) {
|
||||||
|
const filteredTasks = this.chatService.allChats.filter(
|
||||||
|
(chat) => chat.channelId == chatId
|
||||||
|
);
|
||||||
|
return filteredTasks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<section (click)="openSearchbar($event)">
|
||||||
|
<div class="whiteBox">
|
||||||
|
<input type="text" placeholder="To: #channel or @someone" [(ngModel)]="inputValue">
|
||||||
|
<img src="./assets/img/search-icon.svg" (click)="closeSearchWindow($event)">
|
||||||
|
</div>
|
||||||
|
@if (inputValue) {
|
||||||
|
<div class="suggestions"></div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
@import "./../../../../styles.scss";
|
||||||
|
|
||||||
|
section{
|
||||||
|
position: relative;
|
||||||
|
@include displayFlex();
|
||||||
|
.whiteBox{
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
top: -16px;
|
||||||
|
left: 322px;
|
||||||
|
@include displayFlex();
|
||||||
|
width: auto;
|
||||||
|
height: 56px;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 2px 8px 2px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 34px;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
box-shadow: rgb(111 139 169) 0px 20px 30px -10px;
|
||||||
|
input{
|
||||||
|
width: 30vw;
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 26px;
|
||||||
|
padding-right: 40px;
|
||||||
|
padding-left: 8px;
|
||||||
|
margin-right: 2px;
|
||||||
|
font-size: larger;
|
||||||
|
border: 1px solid #888dec;
|
||||||
|
font-family: "Nunito", sans-serif;
|
||||||
|
outline: none;
|
||||||
|
&:hover{
|
||||||
|
border: 1px solid #5f66e7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img{
|
||||||
|
margin-left: -36px;
|
||||||
|
padding-right: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions{
|
||||||
|
width: 200px;
|
||||||
|
height: 300px;
|
||||||
|
background-color: coral;
|
||||||
|
position: absolute;
|
||||||
|
top: 46px;
|
||||||
|
left: 360px;
|
||||||
|
border-bottom-left-radius: 25px;
|
||||||
|
border-bottom-right-radius: 25px;
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: rgb(111 139 169) 0px 20px 30px -10px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SearchbarComponent } from './searchbar.component';
|
||||||
|
|
||||||
|
describe('SearchbarComponent', () => {
|
||||||
|
let component: SearchbarComponent;
|
||||||
|
let fixture: ComponentFixture<SearchbarComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SearchbarComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SearchbarComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
38
src/app/components/sidebar/searchbar/searchbar.component.ts
Normal file
38
src/app/components/sidebar/searchbar/searchbar.component.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ToggleBooleanService } from '../../../service/toggle-boolean.service';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-searchbar',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule],
|
||||||
|
templateUrl: './searchbar.component.html',
|
||||||
|
styleUrl: './searchbar.component.scss',
|
||||||
|
})
|
||||||
|
export class SearchbarComponent {
|
||||||
|
inputValue: string = '';
|
||||||
|
|
||||||
|
constructor(private toggleBoolean: ToggleBooleanService) {}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the search window.
|
||||||
|
* @param event The event object.
|
||||||
|
*/
|
||||||
|
closeSearchWindow(event: Event) {
|
||||||
|
this.toggleBoolean.openSearchWindow = false;
|
||||||
|
event.stopPropagation();
|
||||||
|
this.inputValue = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the search bar.
|
||||||
|
* @param event The event object.
|
||||||
|
*/
|
||||||
|
openSearchbar(event: Event){
|
||||||
|
this.toggleBoolean.openSearchWindow = true;
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue