first commit

This commit is contained in:
Chneemann 2024-05-12 11:57:51 +02:00
commit af661cf543
311 changed files with 31070 additions and 0 deletions

16
.editorconfig Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1 @@
{}

13515
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

45
package.json Normal file
View 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"
}
}

View file

@ -0,0 +1,3 @@
<router-outlet></router-outlet>

View file

View 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
View 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
View 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
View 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 },
];

View file

@ -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>

View file

@ -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;
}
}
}

View file

@ -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();
});
});

View file

@ -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();
}
}

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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();
});
});

View file

@ -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}`;
}
}
}

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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();
});
});

View file

@ -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 = '';
}
}

View 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>&commat;{{ 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>&commat;{{ user.firstName }} {{ user.lastName }}</span>
} {{ "info.talkTo2" | translate }}
</p>
} } }
</section>

View 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;
}
}
}

View 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();
});
});

View 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;
}
}

View 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>

View 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;
}
}

View 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();
});
});

View 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);
}
}

View file

@ -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&#64;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&#64;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>

View file

@ -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;
}
}
}
}

View file

@ -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();
});
});

View file

@ -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}`);
}
}
}

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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();
});
});

View file

@ -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 = [];
}
}

View file

@ -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>

View file

@ -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;
}
}
}
}

View file

@ -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();
});
});

View file

@ -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);
}
}

View 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>

View file

@ -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;
}
}
}

View file

@ -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();
});
});

View file

@ -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);
}
}

View 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>

View file

@ -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;
}
}
}

View file

@ -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();
});
});

View file

@ -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);
}
}
}

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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();
});
});

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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;
}
}
}

View file

@ -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();
});
});

View file

@ -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);
}
}

View file

@ -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>

View file

@ -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;
}
}
}
}

View file

@ -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();
});
});

View file

@ -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);
}
}

View 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>

View 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;
}
}

View 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();
});
});

View 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();
}
}

View 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>

View file

@ -0,0 +1,3 @@

View 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();
});
});

View 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 {
}

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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();
});
});

View file

@ -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);
}
}

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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();
});
});

View file

@ -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;
}
}

View 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>

View 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;
}
}

View file

@ -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();
});
});

View 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 = '';
}
}

View 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>

View 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);
}
}

View 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();
});
});

View 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;
}
}

View file

@ -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>

View 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);
}
}

View file

@ -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();
});
});

View 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;
}
}

View file

@ -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>

View file

@ -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;
}

View file

@ -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();
});
});

View 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