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